Subversion Repositories yt_downloader

Rev

Rev 11 | Rev 13 | 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
10 daniel-mar 5
// Revision: 2022-02-05
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
		}
162
		if (!@file_put_contents(__DIR__.'/youtube-dl', file_get_contents('https://yt-dl.org/downloads/latest/youtube-dl'))) {
163
			fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (file_get_contents).\n");
164
		} else {
165
			if (!@chmod(__DIR__.'/youtube-dl', 0544)) {
166
				fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (chmod 544).\n");
167
				@unlink(__DIR__.'/youtube-dl'); // try to delete, otherwise we might try to execute a non-executable file
168
			}
169
		}
170
	}
171
}
172
 
2 daniel-mar 173
if (command_exists(__DIR__.'/youtube-dl')) {
5 daniel-mar 174
	echo "Will use 'youtube-dl' from local directory\n";
2 daniel-mar 175
	define('YTDL_EXE', __DIR__.'/youtube-dl');
176
} else {
5 daniel-mar 177
	// Download failed. Is at least a package installed?
178
	if (command_exists('youtube-dl')) {
179
		echo "Will use 'youtube-dl' from Linux package\n";
180
		define('YTDL_EXE', 'youtube-dl');
2 daniel-mar 181
	} else {
182
		fwrite(STDERR, "This script requires the tool/package 'youtube-dl'. Please install it first.\n");
183
		exit(1);
184
	}
185
}
186
 
187
// Now process the videos
188
 
189
yt_set_apikey_callback('_getApikey');
190
 
191
foreach ($rest_args as $resource) {
192
	if ($verbose) echo "Handle: $resource\n";
193
	if (strpos($resource, ':') === false) {
194
		fwrite(STDERR, "Invalid resource '$resource' (you are missing the prefix, e.g. vurl: or vid:). Skipping.\n");
195
	} else {
196
		list($resourceType, $resourceValue) = explode(':', $resource, 2);
197
		ytdwn_handle_resource($resourceType, $resourceValue);
198
	}
199
}
200
 
201
// ------------------------------------------------------------------------------------------------
202
 
203
function ytdwn_handle_resource($resourceType, $resourceValue) {
204
	if ($resourceType == 'vid') {
205
		$video_id = parse_quoting($resourceValue);
206
		ytdwn_video_id($video_id);
207
	} else if ($resourceType == 'vurl') {
208
		$video_url = parse_quoting($resourceValue);
209
		$video_id  = getVideoIDFromURL($video_url);
210
		if (!$video_id) {
211
			fwrite(STDERR, "$video_url is not a valid YouTube video URL. Skipping.\n");
212
		} else {
213
			ytdwn_video_id($video_id);
214
		}
215
	} else if ($resourceType == 'pid') {
216
		$playlist_id = parse_quoting($resourceValue);
217
		ytdwn_playlist_id($playlist_id);
218
	} else if ($resourceType == 'purl') {
219
		$playlist_url = parse_quoting($resourceValue);
220
		$playlist_id  = getPlaylistIDFromURL($playlist_url);
221
		if (!$playlist_id) {
222
			fwrite(STDERR, "$playlist_url is not a valid YouTube playlist URL. Skipping\n");
223
		} else {
224
			ytdwn_playlist_id($playlist_id);
225
		}
226
	} else if ($resourceType == 'cid') {
227
		$channel_id = parse_quoting($resourceValue);
228
 
7 daniel-mar 229
		$m = null;
2 daniel-mar 230
		if (preg_match('@\[search=(.+)\]@ismU', $channel_id, $m)) {
231
			$search = $m[1];
232
			$channel_id = preg_replace('@\[search=(.+)\]@ismU', '', $channel_id);
233
		} else {
234
			$search = ''; // default
235
		}
236
		$search = parse_quoting($search);
237
 
238
		ytdwn_channel_id($channel_id, $search);
239
	} else if ($resourceType == 'cname') {
240
		$channel_name = parse_quoting($resourceValue);
241
 
7 daniel-mar 242
		$m = null;
2 daniel-mar 243
		if (preg_match('@\[search=(.+)\]@ismU', $channel_name, $m)) {
244
			$search = $m[1];
245
			$channel_name = preg_replace('@\[search=(.+)\]@ismU', '', $channel_name);
246
		} else {
247
			$search = ''; // default
248
		}
249
		$search = parse_quoting($search);
250
 
251
		$channel_name = parse_quoting($channel_name);
252
		$channel_id = yt_get_channel_id($channel_name);
253
		if (!$channel_id) {
254
			fwrite(STDERR, "URL $channel_name is a valid YouTube channel or username. Skipping.\n");
255
		} else {
256
			ytdwn_channel_id($channel_id, $search);
257
		}
258
	} else if ($resourceType == 'curl') {
259
		$channel_url = parse_quoting($resourceValue);
260
 
7 daniel-mar 261
		$m = null;
2 daniel-mar 262
		if (preg_match('@\[search=(.+)\]@ismU', $channel_url, $m)) {
263
			$search = $m[1];
264
			$channel_url = preg_replace('@\[search=(.+)\]@ismU', '', $channel_url);
265
		} else {
266
			$search = ''; // default
267
		}
268
		$search = parse_quoting($search);
269
 
270
		$channel_url = parse_quoting($channel_url);
271
		$channel_id = curl_to_cid($channel_url);
272
		if (!$channel_id) {
8 daniel-mar 273
			fwrite(STDERR, "URL $channel_url is a valid YouTube channel or username URL. Skipping\n");
2 daniel-mar 274
		} else {
275
			ytdwn_channel_id($channel_id, $search);
276
		}
277
	} else if ($resourceType == 'search') {
278
		$searchterm = parse_quoting($resourceValue);
279
 
280
		$order = '';
7 daniel-mar 281
		$m = null;
2 daniel-mar 282
		if (preg_match('@\[order=(.+)\]@ismU', $searchterm, $m)) {
283
			$order = $m[1];
284
			$searchterm = preg_replace('@\[order=(.+)\]@ismU', '', $searchterm);
285
		} else {
286
			$order = DEFAULT_SEARCH_ORDER; // default
287
		}
288
		$order = parse_quoting($order);
289
 
290
		$maxresults = '';
291
		if (preg_match('@\[maxresults=(.+)\]@ismU', $searchterm, $m)) {
292
			$maxresults = $m[1];
293
			$searchterm = preg_replace('@\[maxresults=(.+)\]@ismU', '', $searchterm);
294
		} else {
295
			$maxresults = DEFAULT_SEARCH_MAXRESULTS; // default
296
		}
297
		$maxresults = parse_quoting($maxresults);
298
 
299
		$searchterm = parse_quoting($searchterm);
300
 
301
		ytdwn_search($searchterm, $order, $maxresults);
302
	} else if ($resourceType == 'list') {
303
		$list_files = glob(parse_quoting($resourceValue)); // in case the user entered a wildcard, e.g. *.list
304
		foreach ($list_files as $list_file) {
305
			if (!file_exists($list_file)) {
306
				fwrite(STDERR, "List file $list_file does not exist. Skipping\n");
307
			} else {
308
				ytdwn_list_file($list_file);
309
			}
310
		}
311
	} else {
312
		fwrite(STDERR, "Resource type '$resourceType' is not valid. Skipping $resourceType:$resourceValue.\n");
313
	}
314
}
315
 
316
function ytdwn_list_file($list_file) {
317
	global $listFilenameStack, $verbose;
318
 
319
	if ($verbose) echo "Processing list file '$list_file' ...\n";
320
 
321
	$listFilenameStack[] = $list_file;
322
	$lines = file($list_file);
323
	foreach ($lines as $line) {
324
		$line = trim($line);
325
		if ($line == '') continue;
326
		if ($line[0] == '#') continue;
327
		if (strpos($line, ':') === false) {
328
			fwrite(STDERR, "Invalid resource '$line' (you are missing the prefix, e.g. vurl: or vid:). Skipping.\n");
329
		} else {
330
			list($resourceType, $resourceValue) = explode(':',$line,2);
331
			ytdwn_handle_resource($resourceType, $resourceValue);
332
		}
333
	}
334
	array_pop($listFilenameStack);
335
}
336
 
337
function ytdwn_channel_id($channel_id, $search='') {
338
	global $type;
339
	global $verbose;
340
 
341
	if ($verbose) echo "Processing channel ID '$channel_id' ...\n";
342
 
343
	// List the videos of the channel
344
 
345
	$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
346
	$out = json_decode($cont, true);
347
	if ($out == NULL) $out = array();
348
 
349
	if (!empty(_getResultcache())) {
350
		$stats = yt_get_channel_stats($channel_id);
11 daniel-mar 351
		if ($stats === false) {
352
			fwrite(STDERR, "Cannot get stats for channel with ID '$channel_id'\n");
353
			return;
354
		}
2 daniel-mar 355
		$videocount = $stats['videoCount'];
356
 
357
		$key = (!empty($search)) ? 'cid:'.$channel_id.'/'.$search : 'cid:'.$channel_id;
358
 
359
		if (!isset($out[$key])) $out[$key] = array();
360
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
361
	} else {
362
		$videocount = -1;
363
		$videocount_old = -2;
364
		$key = '';
365
	}
366
 
367
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
368
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
369
		$out[$key]['count'] = $videocount;
370
		if (!empty($search)) {
371
			$out[$key]['results'] = yt_channel_items($channel_id, $search);
372
		} else {
373
			$out[$key]['results'] = yt_channel_items($channel_id);
374
		}
375
	} else {
376
		if ($verbose) echo "Video count for channel is still $videocount, keep ".count($out[$key]['results'])." results.\n";
377
	}
378
 
379
	// Save the cache
380
 
381
	try {
382
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
383
	} catch(Exception $e) {
384
		fwrite(STDERR, "Cannot write result cache\n");
385
	}
386
 
387
	// Now download
388
 
6 daniel-mar 389
	if (!$out[$key]['results']) {
390
		fwrite(STDERR, "Cannot get result for channel with ID '$channel_id'\n");
391
		return;
392
	}
2 daniel-mar 393
	foreach ($out[$key]['results'] as list($id, $title)) {
394
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
395
		ytdwn_video_id($id);
396
	}
397
}
398
 
399
function ytdwn_playlist_id($playlist_id) {
400
	global $type;
401
	global $verbose;
402
 
403
	if ($verbose) echo "Processing playlist ID '$playlist_id' ...\n";
404
 
405
	// List the videos of the playlist
406
 
407
	$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
408
	$out = json_decode($cont, true);
409
	if ($out == NULL) $out = array();
410
 
411
	if (!empty(_getResultcache())) {
412
		$stats = yt_get_playlist_stats($playlist_id);
11 daniel-mar 413
		if ($stats === false) {
12 daniel-mar 414
			fwrite(STDERR, "Cannot get stats for playlist with ID '$playlist_id'\n");
11 daniel-mar 415
			return;
416
		}
2 daniel-mar 417
		$videocount = $stats['itemCount'];
418
 
419
		$key = 'pid:'.$playlist_id;
420
 
421
		if (!isset($out[$key])) $out[$key] = array();
422
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
423
	} else {
424
		$videocount = -1;
425
		$videocount_old = -2;
426
		$key = '';
427
	}
428
 
429
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
430
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
431
		$out[$key]['count'] = $videocount;
432
		$out[$key]['results'] = yt_playlist_items($playlist_id);
433
	} else {
434
		if ($verbose) echo "Video count for playlist is still $videocount, keep ".count($out[$key]['results'])." results.\n";
435
	}
436
 
437
	// Save the cache
438
 
439
	try {
440
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
441
	} catch(Exception $e) {
442
		fwrite(STDERR, "Cannot write result cache\n");
443
	}
444
 
445
	// Now download
446
 
6 daniel-mar 447
	if (!$out[$key]['results']) {
448
		fwrite(STDERR, "Cannot get result for playlist with ID '$playlist_id'\n");
449
		return;
450
	}
2 daniel-mar 451
	foreach ($out[$key]['results'] as list($id, $title)) {
452
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
453
		ytdwn_video_id($id);
454
	}
455
}
456
 
457
function ytdwn_search($search, $order='', $maxresults=-1) {
458
	global $type;
459
	global $verbose;
460
 
461
	if ($verbose) echo "Searching for '$search' ...\n";
462
 
463
	// Perform the search and list the videos
464
 
465
	$results = yt_search_items($search, $order, $maxresults);
466
 
467
	// Now download
468
 
6 daniel-mar 469
	if (!$results) {
470
		fwrite(STDERR, "Cannot get data for search '$search'\n");
471
		return;
472
	}
2 daniel-mar 473
	foreach ($results as list($id, $title)) {
474
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
475
		ytdwn_video_id($id);
476
	}
477
}
478
 
479
function ytdwn_video_id($video_id) {
480
	global $type;
481
	global $verbose;
482
	global $mp3id_transfer;
483
	global $extra_args;
484
	global $default_template;
485
	global $failTreshold;
6 daniel-mar 486
	global $cookie_file;
2 daniel-mar 487
 
488
	if (DOWNLOAD_SIMULATION_MODE) {
489
		echo "SIMULATE download of video id $video_id as ".hf_type($type)." to "._getOutputDir()."\n";
490
		return;
491
	}
492
 
493
	if (!empty(_getAlreadyDownloaded()) && in_alreadydownloaded_file($type, $video_id)) {
494
		if ($verbose) echo "Video $video_id has already been downloaded. Skip.\n";
495
		return true;
496
	}
497
 
498
	if (!empty(_getFailList()) && (ytdwn_fail_counter($type, $video_id) >= $failTreshold)) {
499
		if ($verbose) echo "Video $video_id has failed too often. Skip.\n";
500
		return true;
501
	}
502
 
503
	$out = '';
504
	$code = -1;
505
 
506
	$outputTemplate = rtrim(_getOutputDir(), '/').'/'.$default_template;
507
 
508
	if (substr($type,0,2) == 'v:') {
509
		$format = substr($type,2);
510
		if (!empty($format)) {
6 daniel-mar 511
			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 512
		} else {
6 daniel-mar 513
			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 514
		}
515
	} else if (substr($type,0,2) == 'a:') {
516
		$format = substr($type,2);
517
		if (!empty($format)) {
6 daniel-mar 518
			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 519
		} else {
6 daniel-mar 520
			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 521
		}
522
		if (($mp3id_transfer) && ($format == 'mp3')) mp3_transfer_vid_to_id();
523
	} else assert(false);
524
 
525
	if ($code == 0) {
526
		if ($verbose) fwrite(STDOUT, "Successfully downloaded video ID $video_id as ".hf_type($type)."\n");
527
		if (!empty(_getAlreadyDownloaded())) {
528
			try {
529
				addto_alreadydownloaded_file($type, $video_id);
530
			} catch(Exception $e) {
531
				fwrite(STDERR, "Cannot add to already downloaded file\n");
532
			}
533
		}
534
	} else {
535
		fwrite(STDERR, "Error downloading $video_id! (Code $code)\n");
536
		if (!empty(_getFailList())) {
537
			try {
538
				ytdwn_register_fail($type, $video_id, $code);
539
			} catch(Exception $e) {
540
				fwrite(STDERR, "Cannot register fail\n");
541
			}
542
		}
543
		return false;
544
	}
545
 
546
	return true;
547
}
548
 
549
function vid_to_vurl($video_id) {
7 daniel-mar 550
	return "https://www.youtube.com/watch?v=$video_id";
2 daniel-mar 551
}
552
 
553
function EndsWith($Haystack, $Needle){
554
	return strrpos($Haystack, $Needle) === strlen($Haystack)-strlen($Needle);
555
}
556
 
557
function mp3_transfer_vid_to_id() {
558
	global $verbose;
559
	global $default_template;
560
 
561
	if (!command_exists('id3v2')) {
562
		if ($verbose) echo "Note: Tool id3v2 is not installed. Will not transfer the YouTube ID into the MP3 ID Tag\n";
563
		return false;
564
	}
565
 
566
	if (!EndsWith($default_template, '-%(id)s.%(ext)s'))  {
567
		if ($verbose) echo "Note: Cannot transfer video tag to MP3 because default template does not end with '-%(id)s.%(ext)s'.\n";
568
		return false;
569
	}
570
 
571
	$allok = true;
572
	$files = glob(rtrim(_getOutputDir(), '/').'/*-???????????.mp3');
573
	foreach ($files as $filename) {
7 daniel-mar 574
		$m = null;
2 daniel-mar 575
		if (!preg_match('@-([a-zA-Z0-9\-_]{11})\.mp3$@ismU', $filename, $m)) continue;
576
		$yt_id = $m[1];
577
 
578
		if (!yt_check_video_id($yt_id)) continue; // just to be sure
579
 
580
		$orig_ts = filemtime($filename);
7 daniel-mar 581
		$ec = -1;
2 daniel-mar 582
		system('id3v2 -c '.escapeshellarg($yt_id).' '.escapeshellarg($filename), $ec);
583
		touch($filename, $orig_ts);
584
		if ($ec != 0) {
585
			fwrite(STDERR, "Cannot set ID tag for file $filename\n");
586
			$allok = false;
587
			continue;
588
		}
589
 
590
		$target_filename = str_replace("-$yt_id.mp3", '.mp3', $filename);
591
		if (!intelligent_rename($filename, $target_filename)) {
592
			fwrite(STDERR, "Cannot move file $filename to $target_filename\n");
593
			$allok = false;
594
			continue;
595
		}
596
	}
597
	return $allok;
598
}
599
 
600
function curl_to_cid($channel_url) {
7 daniel-mar 601
	$m = null;
8 daniel-mar 602
	if (preg_match("@https{0,1}://(www\\.|)youtube\\.com/user/(.*)(/|&|\\?)@ismU", $channel_url.'&', $m)) {
2 daniel-mar 603
		$username = $m[2];
604
		$channel_id = yt_get_channel_id($username);
605
		return $channel_id;
10 daniel-mar 606
	} else if (preg_match("@https{0,1}://(www\\.|)youtube\\.com/(channel|c)/(.*)(/|&|\\?)@ismU", $channel_url.'&', $m)) {
607
		$channel_id = $m[3];
2 daniel-mar 608
		return $channel_id;
609
	} else {
610
		return false;
611
	}
612
}
613
 
614
function in_alreadydownloaded_file($type, $video_id) {
615
	$lines = file(_getAlreadyDownloaded());
616
	foreach ($lines as $line) {
617
		if (trim($line) == rtrim($type,':').':'.$video_id) {
618
			return true;
619
		}
620
	}
621
	return false;
622
}
623
 
624
function addto_alreadydownloaded_file($type, $video_id) {
625
	file_put_contents(_getAlreadyDownloaded(), rtrim($type,':').':'.$video_id."\n", FILE_APPEND);
626
}
627
 
628
function syntax_error($msg) {
629
	fwrite(STDERR, "Syntax error: ".trim($msg)."\n");
630
	fwrite(STDERR, "Please use argument '--help' to show the syntax rules.\n");
631
	exit(2);
632
}
633
 
634
function _help() {
635
	global $argv;
636
	$out = '';
637
	$own = file_get_contents($argv[0]);
638
	$help = explode('// ----', $own, 2)[0];
7 daniel-mar 639
	$m = null;
2 daniel-mar 640
	$help = preg_match_all('@^//(.*)$@mU', $help, $m);
641
	foreach ($m[1] as $line) {
642
		$out .= substr($line,1)."\n";
643
	}
644
	return $out;
645
}
646
 
647
function help() {
648
	echo _help();
649
	exit(0);
650
}
651
 
652
function version() {
653
	echo explode("\n\n", _help(), 2)[0]."\n";
654
	exit(0);
655
}
656
 
657
function command_exists($command) {
658
	// https://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script
659
 
660
	$ec = -1;
661
	system('command -v '.escapeshellarg($command).' > /dev/null', $ec);
662
	return ($ec == 0);
663
}
664
 
665
function hf_type($type) {
666
	if (strpos($type, ':') === false) return $type; // invalid type (missing ':')
667
	list($av, $format) = explode(':', $type);
668
 
669
	if ($av == 'a') $av = 'audio';
670
	else if ($av == 'v') $av = 'video';
671
	else return $type; // invalid type
672
 
673
	return (!empty($format)) ? $format.'-'.$av : $av;
674
}
675
 
676
function expand_tilde($path) {
677
	// Source: http://jonathonhill.net/2013-09-03/tilde-expansion-in-php/
678
 
679
	if (function_exists('posix_getuid') && strpos($path, '~') !== false) {
680
		$info = posix_getpwuid(posix_getuid());
681
		$path = str_replace('~', $info['dir'], $path);
682
	}
683
 
684
	return $path;
685
}
686
 
687
function _getLastListname() {
688
	global $listFilenameStack;
689
	$listname = ''; // default
690
	if (count($listFilenameStack) > 0) {
691
		$listname = $listFilenameStack[count($listFilenameStack)-1];
692
		$listname = pathinfo($listname, PATHINFO_FILENAME); // remove file extension, e.g. ".list"
693
	}
694
	return $listname;
695
}
696
 
697
function _getApiKey() {
698
	global $apikey;
699
 
700
	$out = $apikey;
701
	if (empty($out)) {
702
		$auto_api_key = AUTO_API_KEY;
703
		$auto_api_key = expand_tilde($auto_api_key);
704
		$auto_api_key = str_replace('[listname]', _getLastListname(), $auto_api_key);
705
 
706
		if (file_exists($auto_api_key)) {
707
			$out = trim(file_get_contents($auto_api_key));
708
		} else {
709
			syntax_error("Please specify a YouTube API key with argument '-A'.");
710
		}
711
	} else {
712
		$out = str_replace('[listname]', _getLastListname(), $out);
713
		$out = expand_tilde($out);
714
 
715
		if (file_exists($out)) {
716
			$out = trim(file_get_contents($out));
717
		} else {
718
			// Assume, $out is a key, not a file
719
		}
720
	}
721
 
722
	if (!yt_check_apikey_syntax($out)) syntax_error("'$out' is not a valid API key, not an existing file containing an API key.\n");
723
 
724
	return $out;
725
}
726
 
727
function _getResultCache() {
728
	global $resultcache;
729
	if (empty($resultcache)) return '';
730
 
731
	$out = expand_tilde($resultcache);
732
 
733
	$out = str_replace('[listname]', _getLastListname(), $out);
734
 
735
	if (!file_exists($out)) {
736
		@touch($out);
737
		if (!file_exists($out)) {
738
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
739
			return '';
740
		}
741
	}
742
 
743
	return $out;
744
}
745
 
746
function _getAlreadyDownloaded() {
747
	global $alreadyDownloaded;
748
	if (empty($alreadyDownloaded)) return '';
749
 
750
	$out = expand_tilde($alreadyDownloaded);
751
 
752
	$out = str_replace('[listname]', _getLastListname(), $out);
753
 
754
	if (!file_exists($out)) {
755
		@touch($out);
756
		if (!file_exists($out)) {
757
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
758
			return '';
759
		}
760
	}
761
 
762
	return $out;
763
}
764
 
765
function _getFailList() {
766
	global $failList;
767
	if (empty($failList)) return '';
768
 
769
	$out = expand_tilde($failList);
770
 
771
	$out = str_replace('[listname]', _getLastListname(), $out);
772
 
773
	if (!file_exists($out)) {
774
		@touch($out);
775
		if (!file_exists($out)) {
776
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
777
			return '';
778
		}
779
	}
780
 
781
	return $out;
782
}
783
 
784
function _getOutputDir() {
785
	global $outputDir, $allow_creation_outputdir;
786
	if (empty($outputDir)) return '.';
787
 
788
	$out = expand_tilde($outputDir);
789
 
790
	$out = str_replace('[listname]', _getLastListname(), $out);
791
 
792
	if ($allow_creation_outputdir) {
793
		if (!is_dir($out)) {
12 daniel-mar 794
			mkdir($out, 0777, true);
2 daniel-mar 795
			if (!is_dir($out)) {
796
				fwrite(STDERR, "Output directory '$out' does not exist.\n");
797
				exit(1);
798
			}
799
		}
800
	} else {
801
		if (!is_dir($out)) {
802
			fwrite(STDERR, "Output directory '$out' does not exist.\n");
803
			exit(1);
804
		}
805
	}
806
 
807
	return $out;
808
}
809
 
810
function parse_quoting($str) {
811
	if ((substr($str,0,1) == '"') && (substr($str,-1,1) == '"')) {
812
		$str = substr($str,1,strlen($str)-2);
813
 
814
		$escape = false;
815
		$out = '';
816
		for ($i=0; $i<strlen($str); $i++) {
817
			$char = $str[$i];
818
 
819
			if ($char == '\\') {
820
				if ($escape) {
821
					$out .= $char;
822
					$escape = false;
823
				} else {
824
					$escape = true;
825
				}
826
			} else {
827
				$out .= $char;
828
			}
829
 
830
		}
831
		$str = $out;
832
 
833
	}
834
 
835
	return $str;
836
}
837
 
838
function ytdwn_register_fail($type, $video_id, $code) {
839
	// Note: Error code $code ist currently not used
840
 
841
	$file = _getFailList();
842
	$cont = file_get_contents($file);
7 daniel-mar 843
	$m = null;
2 daniel-mar 844
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
845
		$cont = preg_replace("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU",
846
		                     "Video ID $video_id failed ".($m[1]+1)." time(s) with type $type", $cont);
847
		file_put_contents($file, $cont);
848
	} else {
849
		file_put_contents($file, "Video ID $video_id failed 1 time(s) with type $type\n", FILE_APPEND);
850
	}
851
}
852
 
853
function ytdwn_fail_counter($type, $video_id) {
854
	$file = _getFailList();
855
	$cont = file_get_contents($file);
7 daniel-mar 856
	$m = null;
2 daniel-mar 857
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
858
		return $m[1];
859
	} else {
860
		return 0;
861
	}
862
}
863
 
864
function intelligent_rename($src, $dest) {
865
	$pos = strrpos($dest, '.');
866
	$ext = substr($dest, $pos);
867
	$namewoext = substr($dest, 0, $pos);
868
	$failcnt = 1;
869
	$dest_neu = $dest;
870
	while (file_exists($dest_neu)) {
871
		$failcnt++;
872
		$dest_neu = "$namewoext ($failcnt)$ext";
873
	}
874
	return rename($src, $dest_neu);
875
}
5 daniel-mar 876
 
877
function get_latest_ytdl_md5sum() {
878
	$ch = curl_init();
879
	curl_setopt($ch, CURLOPT_URL, 'https://yt-dl.org/downloads/latest/MD5SUMS');
880
	#curl_setopt($ch, CURLOPT_HEADER, false);
881
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
882
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
883
	$cont = curl_exec($ch);
7 daniel-mar 884
	$m = null;
5 daniel-mar 885
	if (preg_match('@^(.+)  youtube\-dl$@ismU', $cont, $m)) {
886
		return $m[1];
887
	} else {
888
		return false;
889
	}
890
}