Subversion Repositories yt_downloader

Rev

Rev 12 | Rev 14 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 daniel-mar 1
#!/usr/bin/php
2
<?php
3
 
6 daniel-mar 4
// ViaThinkSoft YouTube Downloader Util 2.2
13 daniel-mar 5
// Revision: 2022-02-06
2 daniel-mar 6
// Author: Daniel Marschall <www.daniel-marschall.de>
7 daniel-mar 7
// Licensed under the terms of the Apache 2.0 License
2 daniel-mar 8
//
6 daniel-mar 9
// For syntax and other documentation, please read the file README.
2 daniel-mar 10
 
11
// ------------------------------------------------------------------------------------------------
12
 
13
error_reporting(E_ALL | E_NOTICE | E_STRICT | E_DEPRECATED);
14
 
15
define('AUTO_API_KEY', '~/.yt_api_key');
6 daniel-mar 16
define('AUTO_COOKIE_FILE', '~/.yt_cookies');
2 daniel-mar 17
define('DOWNLOAD_SIMULATION_MODE', false);
18
define('DEFAULT_SEARCH_ORDER', 'relevance');
19
define('DEFAULT_SEARCH_MAXRESULTS', 10);
20
 
21
putenv("LANG=de_DE.UTF-8"); // required if video titles contain non-ASCII symbols
22
 
23
require_once __DIR__ . '/youtube_functions.inc.phps';
24
 
25
// Check if we are running in command line
26
 
27
if (PHP_SAPI !== 'cli') {
28
	fwrite(STDERR, "Error: Can only run in CLI mode\n");
29
	exit(2);
30
}
31
 
32
// Global vars
33
 
34
$listFilenameStack = array();
35
 
36
// Default values
37
 
38
$allow_creation_outputdir = false;
39
$type = 'v:';
40
$outputDir = '';
41
$alreadyDownloaded = '';
42
$failList = '';
43
$failTreshold = 3;
44
$rest_args = array();
45
$verbose = false;
46
$mp3id_transfer = true;
47
$apikey = '';
48
$resultcache = '';
49
$extra_args =
50
//            '-k ' . // The additional "-k" option in the above makes youtube-dl keep downloaded videos.
51
              '-i ' . // continue upon download errors
52
              '-c ';  // resume partially downloaded video files
53
$default_template = '%(title)s-%(id)s.%(ext)s';
6 daniel-mar 54
$cookie_file = AUTO_COOKIE_FILE;
2 daniel-mar 55
 
56
// Parse arguments
57
// We do not use getopt() at the moment, because the important functionality "optind" is only available in PHP 7.1, which is not yet distributed with most of the stable Linux distros
58
 
59
$init_extra_args = false;
7 daniel-mar 60
$argv_bak = $_SERVER['argv'];
2 daniel-mar 61
array_shift($argv_bak);
62
while (count($argv_bak) > 0) {
63
	$arg = array_shift($argv_bak);
64
	$arg2 = $arg . ' ' . (isset($argv_bak[0]) ? $argv_bak[0] : '');
7 daniel-mar 65
	$m = null;
2 daniel-mar 66
	if (preg_match('@^(/t|\-t|\-\-type)(\s+|=)(.*)$@s', $arg2, $m)) {
67
		array_shift($argv_bak);
68
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
69
		$type = $m[3];
70
	} else if (preg_match('@^(/o|\-o|\-\-outputDir)(\s+|=)(.*)$@s', $arg2, $m)) {
71
		array_shift($argv_bak);
72
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
73
		$outputDir = $m[3];
74
	} else if (preg_match('@^(/a|\-a|\-\-alreadyDownloaded)(\s+|=)(.*)$@s', $arg2, $m)) {
75
		array_shift($argv_bak);
76
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
77
		$alreadyDownloaded = $m[3];
78
	} else if (preg_match('@^(/f|\-f|\-\-failList)(\s+|=)(.*)$@s', $arg2, $m)) {
79
		array_shift($argv_bak);
80
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
81
		$failList = $m[3];
82
	} else if (preg_match('@^(/F|\-F|\-\-failTreshold)(\s+|=)(.*)$@s', $arg2, $m)) {
83
		array_shift($argv_bak);
84
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
85
		$failTreshold = $m[3];
86
	} else if (preg_match('@^(/C|\-C|\-\-resultcache)(\s+|=)(.*)$@s', $arg2, $m)) {
87
		array_shift($argv_bak);
88
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
89
		$resultcache = $m[3];
90
	} else if (preg_match('@^(/T|\-T|\-\-default-template)(\s+|=)(.*)$@s', $arg2, $m)) {
91
		array_shift($argv_bak);
92
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
93
		$default_template = $m[3];
94
	} else if (preg_match('@^(/A|\-A|\-\-api-key)(\s+|=)(.*)$@s', $arg2, $m)) {
95
		array_shift($argv_bak);
96
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
97
		$apikey = file_exists($m[3]) ? trim(file_get_contents($m[3])) : $m[3];
6 daniel-mar 98
	} else if (preg_match('@^(\-\-cookies)(\s+|=)(.*)$@s', $arg2, $m)) {
99
		array_shift($argv_bak);
100
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
101
		$cookie_file = file_exists($m[3]) ? trim(file_get_contents($m[3])) : $m[3];
2 daniel-mar 102
	} else if (preg_match('@^(/X|\-X|\-\-extra-args)(\s+|=)(.*)$@s', $arg2, $m)) {
103
		array_shift($argv_bak);
104
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
105
		if ($init_extra_args) {
106
			$extra_args .= ' ' . $m[3]; // user has multiple "-X" arguments. append.
107
		} else {
108
			$extra_args = $m[3]; // overwrite defaults
109
			$init_extra_args = true;
110
		}
111
	} else if (preg_match('@^(/\?|/h|\-\?|\-h|\-\-help)$@s', $arg, $m)) {
112
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
113
		help();
114
	} else if (preg_match('@^(/V|\-V|\-\-version)$@s', $arg, $m)) {
115
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
116
		version();
117
	} else if (preg_match('@^(/v|\-v|\-\-verbose)$@s', $arg, $m)) {
118
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
119
		$verbose = true;
120
	} else if (preg_match('@^(/N|\-N|\-\-no-mp3-tagtransfer)$@s', $arg, $m)) {
121
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
122
		$mp3id_transfer = false;
123
	} else if (preg_match('@^(/O|\-O|\-\-create-outputdir)$@s', $arg, $m)) {
124
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
125
		$allow_creation_outputdir = true;
126
	} else if ($arg == '--') {
127
		if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
128
		$rest_args = $argv_bak;
129
		break;
130
	} else {
131
		$rest_args[] = $arg;
132
	}
133
}
134
unset($arg);
135
unset($argv_bak);
136
unset($init_extra_args);
137
 
138
// Validity checks
139
 
140
if ((substr($type,0,2) != 'a:') && (substr($type,0,2) != 'v:')) syntax_error("Type must be either 'v:' or 'a:'. '$type' is not valid.");
141
 
142
if (count($rest_args) == 0) syntax_error("Please enter at least one desired video for downloading");
143
 
144
if ($failTreshold <= 0) syntax_error("Fail treshold has invalid value. Must be >0.");
145
 
6 daniel-mar 146
$cookie_file = expand_tilde($cookie_file);
147
if (!file_exists($cookie_file)) $cookie_file = '';
148
 
5 daniel-mar 149
// Try to download/update youtube-dl into local directory
2 daniel-mar 150
 
5 daniel-mar 151
$newest_version_md5 = get_latest_ytdl_md5sum();
152
if (!$newest_version_md5) {
153
	fwrite(STDERR, "Failed to get MD5 sum of latest version of 'youtube-dl' from GitHub. Will not try to download/update 'youtube-dl' into local directory.\n");
154
} else {
155
	if (!file_exists(__DIR__.'/youtube-dl') || ($newest_version_md5 != md5_file(__DIR__.'/youtube-dl'))) {
156
		// Try to download/update the file in our directory. It should be the newest available, since YT often breaks downloader
157
		if (file_exists(__DIR__.'/youtube-dl')) {
158
			echo "Trying to update 'youtube-dl' in local directory...\n";
159
		} else {
160
			echo "Trying to download 'youtube-dl' into local directory...\n";
161
		}
13 daniel-mar 162
 
163
		@chmod(__DIR__.'/youtube-dl', 0777); // otherwise we might not be able to write to it
164
 
165
		if (!($binary = file_get_contents('https://yt-dl.org/latest/youtube-dl'))) {
5 daniel-mar 166
			fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (file_get_contents).\n");
13 daniel-mar 167
		} else if (!@file_put_contents(__DIR__.'/youtube-dl', $binary)) {
168
			fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (file_put_contents).\n");
5 daniel-mar 169
		} else {
170
			if (!@chmod(__DIR__.'/youtube-dl', 0544)) {
171
				fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (chmod 544).\n");
172
				@unlink(__DIR__.'/youtube-dl'); // try to delete, otherwise we might try to execute a non-executable file
173
			}
174
		}
175
	}
176
}
177
 
2 daniel-mar 178
if (command_exists(__DIR__.'/youtube-dl')) {
5 daniel-mar 179
	echo "Will use 'youtube-dl' from local directory\n";
2 daniel-mar 180
	define('YTDL_EXE', __DIR__.'/youtube-dl');
181
} else {
5 daniel-mar 182
	// Download failed. Is at least a package installed?
183
	if (command_exists('youtube-dl')) {
184
		echo "Will use 'youtube-dl' from Linux package\n";
185
		define('YTDL_EXE', 'youtube-dl');
2 daniel-mar 186
	} else {
187
		fwrite(STDERR, "This script requires the tool/package 'youtube-dl'. Please install it first.\n");
188
		exit(1);
189
	}
190
}
191
 
192
// Now process the videos
193
 
194
yt_set_apikey_callback('_getApikey');
195
 
196
foreach ($rest_args as $resource) {
197
	if ($verbose) echo "Handle: $resource\n";
198
	if (strpos($resource, ':') === false) {
199
		fwrite(STDERR, "Invalid resource '$resource' (you are missing the prefix, e.g. vurl: or vid:). Skipping.\n");
200
	} else {
201
		list($resourceType, $resourceValue) = explode(':', $resource, 2);
202
		ytdwn_handle_resource($resourceType, $resourceValue);
203
	}
204
}
205
 
206
// ------------------------------------------------------------------------------------------------
207
 
208
function ytdwn_handle_resource($resourceType, $resourceValue) {
209
	if ($resourceType == 'vid') {
210
		$video_id = parse_quoting($resourceValue);
211
		ytdwn_video_id($video_id);
212
	} else if ($resourceType == 'vurl') {
213
		$video_url = parse_quoting($resourceValue);
214
		$video_id  = getVideoIDFromURL($video_url);
215
		if (!$video_id) {
216
			fwrite(STDERR, "$video_url is not a valid YouTube video URL. Skipping.\n");
217
		} else {
218
			ytdwn_video_id($video_id);
219
		}
220
	} else if ($resourceType == 'pid') {
221
		$playlist_id = parse_quoting($resourceValue);
222
		ytdwn_playlist_id($playlist_id);
223
	} else if ($resourceType == 'purl') {
224
		$playlist_url = parse_quoting($resourceValue);
225
		$playlist_id  = getPlaylistIDFromURL($playlist_url);
226
		if (!$playlist_id) {
227
			fwrite(STDERR, "$playlist_url is not a valid YouTube playlist URL. Skipping\n");
228
		} else {
229
			ytdwn_playlist_id($playlist_id);
230
		}
231
	} else if ($resourceType == 'cid') {
232
		$channel_id = parse_quoting($resourceValue);
233
 
7 daniel-mar 234
		$m = null;
2 daniel-mar 235
		if (preg_match('@\[search=(.+)\]@ismU', $channel_id, $m)) {
236
			$search = $m[1];
237
			$channel_id = preg_replace('@\[search=(.+)\]@ismU', '', $channel_id);
238
		} else {
239
			$search = ''; // default
240
		}
241
		$search = parse_quoting($search);
242
 
243
		ytdwn_channel_id($channel_id, $search);
244
	} else if ($resourceType == 'cname') {
245
		$channel_name = parse_quoting($resourceValue);
246
 
7 daniel-mar 247
		$m = null;
2 daniel-mar 248
		if (preg_match('@\[search=(.+)\]@ismU', $channel_name, $m)) {
249
			$search = $m[1];
250
			$channel_name = preg_replace('@\[search=(.+)\]@ismU', '', $channel_name);
251
		} else {
252
			$search = ''; // default
253
		}
254
		$search = parse_quoting($search);
255
 
256
		$channel_name = parse_quoting($channel_name);
257
		$channel_id = yt_get_channel_id($channel_name);
258
		if (!$channel_id) {
13 daniel-mar 259
			fwrite(STDERR, "URL $channel_name is a valid YouTube username. Will now try to interprete it as channel ID instead....\n");
2 daniel-mar 260
		}
13 daniel-mar 261
		ytdwn_channel_id($channel_id, $search);
2 daniel-mar 262
	} else if ($resourceType == 'curl') {
263
		$channel_url = parse_quoting($resourceValue);
264
 
7 daniel-mar 265
		$m = null;
2 daniel-mar 266
		if (preg_match('@\[search=(.+)\]@ismU', $channel_url, $m)) {
267
			$search = $m[1];
268
			$channel_url = preg_replace('@\[search=(.+)\]@ismU', '', $channel_url);
269
		} else {
270
			$search = ''; // default
271
		}
272
		$search = parse_quoting($search);
273
 
274
		$channel_url = parse_quoting($channel_url);
275
		$channel_id = curl_to_cid($channel_url);
276
		if (!$channel_id) {
8 daniel-mar 277
			fwrite(STDERR, "URL $channel_url is a valid YouTube channel or username URL. Skipping\n");
2 daniel-mar 278
		} else {
279
			ytdwn_channel_id($channel_id, $search);
280
		}
281
	} else if ($resourceType == 'search') {
282
		$searchterm = parse_quoting($resourceValue);
283
 
284
		$order = '';
7 daniel-mar 285
		$m = null;
2 daniel-mar 286
		if (preg_match('@\[order=(.+)\]@ismU', $searchterm, $m)) {
287
			$order = $m[1];
288
			$searchterm = preg_replace('@\[order=(.+)\]@ismU', '', $searchterm);
289
		} else {
290
			$order = DEFAULT_SEARCH_ORDER; // default
291
		}
292
		$order = parse_quoting($order);
293
 
294
		$maxresults = '';
295
		if (preg_match('@\[maxresults=(.+)\]@ismU', $searchterm, $m)) {
296
			$maxresults = $m[1];
297
			$searchterm = preg_replace('@\[maxresults=(.+)\]@ismU', '', $searchterm);
298
		} else {
299
			$maxresults = DEFAULT_SEARCH_MAXRESULTS; // default
300
		}
301
		$maxresults = parse_quoting($maxresults);
302
 
303
		$searchterm = parse_quoting($searchterm);
304
 
305
		ytdwn_search($searchterm, $order, $maxresults);
306
	} else if ($resourceType == 'list') {
307
		$list_files = glob(parse_quoting($resourceValue)); // in case the user entered a wildcard, e.g. *.list
308
		foreach ($list_files as $list_file) {
309
			if (!file_exists($list_file)) {
310
				fwrite(STDERR, "List file $list_file does not exist. Skipping\n");
311
			} else {
312
				ytdwn_list_file($list_file);
313
			}
314
		}
315
	} else {
316
		fwrite(STDERR, "Resource type '$resourceType' is not valid. Skipping $resourceType:$resourceValue.\n");
317
	}
318
}
319
 
320
function ytdwn_list_file($list_file) {
321
	global $listFilenameStack, $verbose;
322
 
323
	if ($verbose) echo "Processing list file '$list_file' ...\n";
324
 
325
	$listFilenameStack[] = $list_file;
326
	$lines = file($list_file);
327
	foreach ($lines as $line) {
328
		$line = trim($line);
329
		if ($line == '') continue;
330
		if ($line[0] == '#') continue;
331
		if (strpos($line, ':') === false) {
332
			fwrite(STDERR, "Invalid resource '$line' (you are missing the prefix, e.g. vurl: or vid:). Skipping.\n");
333
		} else {
334
			list($resourceType, $resourceValue) = explode(':',$line,2);
335
			ytdwn_handle_resource($resourceType, $resourceValue);
336
		}
337
	}
338
	array_pop($listFilenameStack);
339
}
340
 
341
function ytdwn_channel_id($channel_id, $search='') {
342
	global $type;
343
	global $verbose;
344
 
345
	if ($verbose) echo "Processing channel ID '$channel_id' ...\n";
346
 
347
	// List the videos of the channel
348
 
349
	$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
350
	$out = json_decode($cont, true);
351
	if ($out == NULL) $out = array();
352
 
353
	if (!empty(_getResultcache())) {
354
		$stats = yt_get_channel_stats($channel_id);
11 daniel-mar 355
		if ($stats === false) {
356
			fwrite(STDERR, "Cannot get stats for channel with ID '$channel_id'\n");
357
			return;
358
		}
2 daniel-mar 359
		$videocount = $stats['videoCount'];
360
 
361
		$key = (!empty($search)) ? 'cid:'.$channel_id.'/'.$search : 'cid:'.$channel_id;
362
 
363
		if (!isset($out[$key])) $out[$key] = array();
364
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
365
	} else {
366
		$videocount = -1;
367
		$videocount_old = -2;
368
		$key = '';
369
	}
370
 
371
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
372
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
373
		$out[$key]['count'] = $videocount;
374
		if (!empty($search)) {
375
			$out[$key]['results'] = yt_channel_items($channel_id, $search);
376
		} else {
377
			$out[$key]['results'] = yt_channel_items($channel_id);
378
		}
379
	} else {
380
		if ($verbose) echo "Video count for channel is still $videocount, keep ".count($out[$key]['results'])." results.\n";
381
	}
382
 
383
	// Save the cache
384
 
385
	try {
386
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
387
	} catch(Exception $e) {
388
		fwrite(STDERR, "Cannot write result cache\n");
389
	}
390
 
391
	// Now download
392
 
6 daniel-mar 393
	if (!$out[$key]['results']) {
394
		fwrite(STDERR, "Cannot get result for channel with ID '$channel_id'\n");
395
		return;
396
	}
2 daniel-mar 397
	foreach ($out[$key]['results'] as list($id, $title)) {
398
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
399
		ytdwn_video_id($id);
400
	}
401
}
402
 
403
function ytdwn_playlist_id($playlist_id) {
404
	global $type;
405
	global $verbose;
406
 
407
	if ($verbose) echo "Processing playlist ID '$playlist_id' ...\n";
408
 
409
	// List the videos of the playlist
410
 
411
	$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
412
	$out = json_decode($cont, true);
413
	if ($out == NULL) $out = array();
414
 
415
	if (!empty(_getResultcache())) {
416
		$stats = yt_get_playlist_stats($playlist_id);
11 daniel-mar 417
		if ($stats === false) {
12 daniel-mar 418
			fwrite(STDERR, "Cannot get stats for playlist with ID '$playlist_id'\n");
11 daniel-mar 419
			return;
420
		}
2 daniel-mar 421
		$videocount = $stats['itemCount'];
422
 
423
		$key = 'pid:'.$playlist_id;
424
 
425
		if (!isset($out[$key])) $out[$key] = array();
426
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
427
	} else {
428
		$videocount = -1;
429
		$videocount_old = -2;
430
		$key = '';
431
	}
432
 
433
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
434
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
435
		$out[$key]['count'] = $videocount;
436
		$out[$key]['results'] = yt_playlist_items($playlist_id);
437
	} else {
438
		if ($verbose) echo "Video count for playlist is still $videocount, keep ".count($out[$key]['results'])." results.\n";
439
	}
440
 
441
	// Save the cache
442
 
443
	try {
444
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
445
	} catch(Exception $e) {
446
		fwrite(STDERR, "Cannot write result cache\n");
447
	}
448
 
449
	// Now download
450
 
6 daniel-mar 451
	if (!$out[$key]['results']) {
452
		fwrite(STDERR, "Cannot get result for playlist with ID '$playlist_id'\n");
453
		return;
454
	}
2 daniel-mar 455
	foreach ($out[$key]['results'] as list($id, $title)) {
456
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
457
		ytdwn_video_id($id);
458
	}
459
}
460
 
461
function ytdwn_search($search, $order='', $maxresults=-1) {
462
	global $type;
463
	global $verbose;
464
 
465
	if ($verbose) echo "Searching for '$search' ...\n";
466
 
467
	// Perform the search and list the videos
468
 
469
	$results = yt_search_items($search, $order, $maxresults);
470
 
471
	// Now download
472
 
6 daniel-mar 473
	if (!$results) {
474
		fwrite(STDERR, "Cannot get data for search '$search'\n");
475
		return;
476
	}
2 daniel-mar 477
	foreach ($results as list($id, $title)) {
478
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
479
		ytdwn_video_id($id);
480
	}
481
}
482
 
483
function ytdwn_video_id($video_id) {
484
	global $type;
485
	global $verbose;
486
	global $mp3id_transfer;
487
	global $extra_args;
488
	global $default_template;
489
	global $failTreshold;
6 daniel-mar 490
	global $cookie_file;
2 daniel-mar 491
 
492
	if (DOWNLOAD_SIMULATION_MODE) {
493
		echo "SIMULATE download of video id $video_id as ".hf_type($type)." to "._getOutputDir()."\n";
494
		return;
495
	}
496
 
497
	if (!empty(_getAlreadyDownloaded()) && in_alreadydownloaded_file($type, $video_id)) {
498
		if ($verbose) echo "Video $video_id has already been downloaded. Skip.\n";
499
		return true;
500
	}
501
 
502
	if (!empty(_getFailList()) && (ytdwn_fail_counter($type, $video_id) >= $failTreshold)) {
503
		if ($verbose) echo "Video $video_id has failed too often. Skip.\n";
504
		return true;
505
	}
506
 
507
	$out = '';
508
	$code = -1;
509
 
510
	$outputTemplate = rtrim(_getOutputDir(), '/').'/'.$default_template;
511
 
512
	if (substr($type,0,2) == 'v:') {
513
		$format = substr($type,2);
514
		if (!empty($format)) {
6 daniel-mar 515
			exec(YTDL_EXE.' -o '.escapeshellarg($outputTemplate).' '.$extra_args.(empty($cookie_file) ? '' : ' --cookies '.$cookie_file).' '.escapeshellarg(vid_to_vurl($video_id)).' --format '.escapeshellarg($format), $out, $code);
2 daniel-mar 516
		} else {
6 daniel-mar 517
			exec(YTDL_EXE.' -o '.escapeshellarg($outputTemplate).' '.$extra_args.(empty($cookie_file) ? '' : ' --cookies '.$cookie_file).' '.escapeshellarg(vid_to_vurl($video_id)), $out, $code);
2 daniel-mar 518
		}
519
	} else if (substr($type,0,2) == 'a:') {
520
		$format = substr($type,2);
521
		if (!empty($format)) {
6 daniel-mar 522
			exec(YTDL_EXE.' -o '.escapeshellarg($outputTemplate).' '.$extra_args.(empty($cookie_file) ? '' : ' --cookies '.$cookie_file).' '.escapeshellarg(vid_to_vurl($video_id)).' --extract-audio --audio-format '.escapeshellarg($format), $out, $code);
2 daniel-mar 523
		} else {
6 daniel-mar 524
			exec(YTDL_EXE.' -o '.escapeshellarg($outputTemplate).' '.$extra_args.(empty($cookie_file) ? '' : ' --cookies '.$cookie_file).' '.escapeshellarg(vid_to_vurl($video_id)).' --extract-audio', $out, $code);
2 daniel-mar 525
		}
526
		if (($mp3id_transfer) && ($format == 'mp3')) mp3_transfer_vid_to_id();
527
	} else assert(false);
528
 
529
	if ($code == 0) {
530
		if ($verbose) fwrite(STDOUT, "Successfully downloaded video ID $video_id as ".hf_type($type)."\n");
531
		if (!empty(_getAlreadyDownloaded())) {
532
			try {
533
				addto_alreadydownloaded_file($type, $video_id);
534
			} catch(Exception $e) {
535
				fwrite(STDERR, "Cannot add to already downloaded file\n");
536
			}
537
		}
538
	} else {
539
		fwrite(STDERR, "Error downloading $video_id! (Code $code)\n");
540
		if (!empty(_getFailList())) {
541
			try {
542
				ytdwn_register_fail($type, $video_id, $code);
543
			} catch(Exception $e) {
544
				fwrite(STDERR, "Cannot register fail\n");
545
			}
546
		}
547
		return false;
548
	}
549
 
550
	return true;
551
}
552
 
553
function vid_to_vurl($video_id) {
7 daniel-mar 554
	return "https://www.youtube.com/watch?v=$video_id";
2 daniel-mar 555
}
556
 
557
function EndsWith($Haystack, $Needle){
558
	return strrpos($Haystack, $Needle) === strlen($Haystack)-strlen($Needle);
559
}
560
 
561
function mp3_transfer_vid_to_id() {
562
	global $verbose;
563
	global $default_template;
564
 
565
	if (!command_exists('id3v2')) {
566
		if ($verbose) echo "Note: Tool id3v2 is not installed. Will not transfer the YouTube ID into the MP3 ID Tag\n";
567
		return false;
568
	}
569
 
570
	if (!EndsWith($default_template, '-%(id)s.%(ext)s'))  {
571
		if ($verbose) echo "Note: Cannot transfer video tag to MP3 because default template does not end with '-%(id)s.%(ext)s'.\n";
572
		return false;
573
	}
574
 
575
	$allok = true;
576
	$files = glob(rtrim(_getOutputDir(), '/').'/*-???????????.mp3');
577
	foreach ($files as $filename) {
7 daniel-mar 578
		$m = null;
2 daniel-mar 579
		if (!preg_match('@-([a-zA-Z0-9\-_]{11})\.mp3$@ismU', $filename, $m)) continue;
580
		$yt_id = $m[1];
581
 
582
		if (!yt_check_video_id($yt_id)) continue; // just to be sure
583
 
584
		$orig_ts = filemtime($filename);
7 daniel-mar 585
		$ec = -1;
2 daniel-mar 586
		system('id3v2 -c '.escapeshellarg($yt_id).' '.escapeshellarg($filename), $ec);
587
		touch($filename, $orig_ts);
588
		if ($ec != 0) {
589
			fwrite(STDERR, "Cannot set ID tag for file $filename\n");
590
			$allok = false;
591
			continue;
592
		}
593
 
594
		$target_filename = str_replace("-$yt_id.mp3", '.mp3', $filename);
595
		if (!intelligent_rename($filename, $target_filename)) {
596
			fwrite(STDERR, "Cannot move file $filename to $target_filename\n");
597
			$allok = false;
598
			continue;
599
		}
600
	}
601
	return $allok;
602
}
603
 
604
function curl_to_cid($channel_url) {
13 daniel-mar 605
	return yt_get_channel_id_from_url($channel_url);
2 daniel-mar 606
}
607
 
608
function in_alreadydownloaded_file($type, $video_id) {
609
	$lines = file(_getAlreadyDownloaded());
610
	foreach ($lines as $line) {
611
		if (trim($line) == rtrim($type,':').':'.$video_id) {
612
			return true;
613
		}
614
	}
615
	return false;
616
}
617
 
618
function addto_alreadydownloaded_file($type, $video_id) {
619
	file_put_contents(_getAlreadyDownloaded(), rtrim($type,':').':'.$video_id."\n", FILE_APPEND);
620
}
621
 
622
function syntax_error($msg) {
623
	fwrite(STDERR, "Syntax error: ".trim($msg)."\n");
624
	fwrite(STDERR, "Please use argument '--help' to show the syntax rules.\n");
625
	exit(2);
626
}
627
 
628
function _help() {
629
	global $argv;
630
	$out = '';
631
	$own = file_get_contents($argv[0]);
632
	$help = explode('// ----', $own, 2)[0];
7 daniel-mar 633
	$m = null;
2 daniel-mar 634
	$help = preg_match_all('@^//(.*)$@mU', $help, $m);
635
	foreach ($m[1] as $line) {
636
		$out .= substr($line,1)."\n";
637
	}
638
	return $out;
639
}
640
 
641
function help() {
642
	echo _help();
643
	exit(0);
644
}
645
 
646
function version() {
647
	echo explode("\n\n", _help(), 2)[0]."\n";
648
	exit(0);
649
}
650
 
651
function command_exists($command) {
652
	// https://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script
653
 
654
	$ec = -1;
655
	system('command -v '.escapeshellarg($command).' > /dev/null', $ec);
656
	return ($ec == 0);
657
}
658
 
659
function hf_type($type) {
660
	if (strpos($type, ':') === false) return $type; // invalid type (missing ':')
661
	list($av, $format) = explode(':', $type);
662
 
663
	if ($av == 'a') $av = 'audio';
664
	else if ($av == 'v') $av = 'video';
665
	else return $type; // invalid type
666
 
667
	return (!empty($format)) ? $format.'-'.$av : $av;
668
}
669
 
670
function expand_tilde($path) {
671
	// Source: http://jonathonhill.net/2013-09-03/tilde-expansion-in-php/
672
 
673
	if (function_exists('posix_getuid') && strpos($path, '~') !== false) {
674
		$info = posix_getpwuid(posix_getuid());
675
		$path = str_replace('~', $info['dir'], $path);
676
	}
677
 
678
	return $path;
679
}
680
 
681
function _getLastListname() {
682
	global $listFilenameStack;
683
	$listname = ''; // default
684
	if (count($listFilenameStack) > 0) {
685
		$listname = $listFilenameStack[count($listFilenameStack)-1];
686
		$listname = pathinfo($listname, PATHINFO_FILENAME); // remove file extension, e.g. ".list"
687
	}
688
	return $listname;
689
}
690
 
691
function _getApiKey() {
692
	global $apikey;
693
 
694
	$out = $apikey;
695
	if (empty($out)) {
696
		$auto_api_key = AUTO_API_KEY;
697
		$auto_api_key = expand_tilde($auto_api_key);
698
		$auto_api_key = str_replace('[listname]', _getLastListname(), $auto_api_key);
699
 
700
		if (file_exists($auto_api_key)) {
701
			$out = trim(file_get_contents($auto_api_key));
702
		} else {
703
			syntax_error("Please specify a YouTube API key with argument '-A'.");
704
		}
705
	} else {
706
		$out = str_replace('[listname]', _getLastListname(), $out);
707
		$out = expand_tilde($out);
708
 
709
		if (file_exists($out)) {
710
			$out = trim(file_get_contents($out));
711
		} else {
712
			// Assume, $out is a key, not a file
713
		}
714
	}
715
 
716
	if (!yt_check_apikey_syntax($out)) syntax_error("'$out' is not a valid API key, not an existing file containing an API key.\n");
717
 
718
	return $out;
719
}
720
 
721
function _getResultCache() {
722
	global $resultcache;
723
	if (empty($resultcache)) return '';
724
 
725
	$out = expand_tilde($resultcache);
726
 
727
	$out = str_replace('[listname]', _getLastListname(), $out);
728
 
729
	if (!file_exists($out)) {
730
		@touch($out);
731
		if (!file_exists($out)) {
732
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
733
			return '';
734
		}
735
	}
736
 
737
	return $out;
738
}
739
 
740
function _getAlreadyDownloaded() {
741
	global $alreadyDownloaded;
742
	if (empty($alreadyDownloaded)) return '';
743
 
744
	$out = expand_tilde($alreadyDownloaded);
745
 
746
	$out = str_replace('[listname]', _getLastListname(), $out);
747
 
748
	if (!file_exists($out)) {
749
		@touch($out);
750
		if (!file_exists($out)) {
751
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
752
			return '';
753
		}
754
	}
755
 
756
	return $out;
757
}
758
 
759
function _getFailList() {
760
	global $failList;
761
	if (empty($failList)) return '';
762
 
763
	$out = expand_tilde($failList);
764
 
765
	$out = str_replace('[listname]', _getLastListname(), $out);
766
 
767
	if (!file_exists($out)) {
768
		@touch($out);
769
		if (!file_exists($out)) {
770
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
771
			return '';
772
		}
773
	}
774
 
775
	return $out;
776
}
777
 
778
function _getOutputDir() {
779
	global $outputDir, $allow_creation_outputdir;
780
	if (empty($outputDir)) return '.';
781
 
782
	$out = expand_tilde($outputDir);
783
 
784
	$out = str_replace('[listname]', _getLastListname(), $out);
785
 
786
	if ($allow_creation_outputdir) {
787
		if (!is_dir($out)) {
12 daniel-mar 788
			mkdir($out, 0777, true);
2 daniel-mar 789
			if (!is_dir($out)) {
790
				fwrite(STDERR, "Output directory '$out' does not exist.\n");
791
				exit(1);
792
			}
793
		}
794
	} else {
795
		if (!is_dir($out)) {
796
			fwrite(STDERR, "Output directory '$out' does not exist.\n");
797
			exit(1);
798
		}
799
	}
800
 
801
	return $out;
802
}
803
 
804
function parse_quoting($str) {
805
	if ((substr($str,0,1) == '"') && (substr($str,-1,1) == '"')) {
806
		$str = substr($str,1,strlen($str)-2);
807
 
808
		$escape = false;
809
		$out = '';
810
		for ($i=0; $i<strlen($str); $i++) {
811
			$char = $str[$i];
812
 
813
			if ($char == '\\') {
814
				if ($escape) {
815
					$out .= $char;
816
					$escape = false;
817
				} else {
818
					$escape = true;
819
				}
820
			} else {
821
				$out .= $char;
822
			}
823
 
824
		}
825
		$str = $out;
826
 
827
	}
828
 
829
	return $str;
830
}
831
 
832
function ytdwn_register_fail($type, $video_id, $code) {
833
	// Note: Error code $code ist currently not used
834
 
835
	$file = _getFailList();
836
	$cont = file_get_contents($file);
7 daniel-mar 837
	$m = null;
2 daniel-mar 838
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
839
		$cont = preg_replace("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU",
840
		                     "Video ID $video_id failed ".($m[1]+1)." time(s) with type $type", $cont);
841
		file_put_contents($file, $cont);
842
	} else {
843
		file_put_contents($file, "Video ID $video_id failed 1 time(s) with type $type\n", FILE_APPEND);
844
	}
845
}
846
 
847
function ytdwn_fail_counter($type, $video_id) {
848
	$file = _getFailList();
849
	$cont = file_get_contents($file);
7 daniel-mar 850
	$m = null;
2 daniel-mar 851
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
852
		return $m[1];
853
	} else {
854
		return 0;
855
	}
856
}
857
 
858
function intelligent_rename($src, $dest) {
859
	$pos = strrpos($dest, '.');
860
	$ext = substr($dest, $pos);
861
	$namewoext = substr($dest, 0, $pos);
862
	$failcnt = 1;
863
	$dest_neu = $dest;
864
	while (file_exists($dest_neu)) {
865
		$failcnt++;
866
		$dest_neu = "$namewoext ($failcnt)$ext";
867
	}
868
	return rename($src, $dest_neu);
869
}
5 daniel-mar 870
 
871
function get_latest_ytdl_md5sum() {
872
	$ch = curl_init();
873
	curl_setopt($ch, CURLOPT_URL, 'https://yt-dl.org/downloads/latest/MD5SUMS');
874
	#curl_setopt($ch, CURLOPT_HEADER, false);
875
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
876
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
877
	$cont = curl_exec($ch);
7 daniel-mar 878
	$m = null;
5 daniel-mar 879
	if (preg_match('@^(.+)  youtube\-dl$@ismU', $cont, $m)) {
880
		return $m[1];
881
	} else {
882
		return false;
883
	}
884
}