Subversion Repositories yt_downloader

Rev

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