Subversion Repositories yt_downloader

Rev

Rev 7 | Rev 10 | 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
8 daniel-mar 5
// Revision: 2021-07-15
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);
351
		$videocount = $stats['videoCount'];
352
 
353
		$key = (!empty($search)) ? 'cid:'.$channel_id.'/'.$search : 'cid:'.$channel_id;
354
 
355
		if (!isset($out[$key])) $out[$key] = array();
356
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
357
	} else {
358
		$videocount = -1;
359
		$videocount_old = -2;
360
		$key = '';
361
	}
362
 
363
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
364
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
365
		$out[$key]['count'] = $videocount;
366
		if (!empty($search)) {
367
			$out[$key]['results'] = yt_channel_items($channel_id, $search);
368
		} else {
369
			$out[$key]['results'] = yt_channel_items($channel_id);
370
		}
371
	} else {
372
		if ($verbose) echo "Video count for channel is still $videocount, keep ".count($out[$key]['results'])." results.\n";
373
	}
374
 
375
	// Save the cache
376
 
377
	try {
378
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
379
	} catch(Exception $e) {
380
		fwrite(STDERR, "Cannot write result cache\n");
381
	}
382
 
383
	// Now download
384
 
6 daniel-mar 385
	if (!$out[$key]['results']) {
386
		fwrite(STDERR, "Cannot get result for channel with ID '$channel_id'\n");
387
		return;
388
	}
2 daniel-mar 389
	foreach ($out[$key]['results'] as list($id, $title)) {
390
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
391
		ytdwn_video_id($id);
392
	}
393
}
394
 
395
function ytdwn_playlist_id($playlist_id) {
396
	global $type;
397
	global $verbose;
398
 
399
	if ($verbose) echo "Processing playlist ID '$playlist_id' ...\n";
400
 
401
	// List the videos of the playlist
402
 
403
	$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
404
	$out = json_decode($cont, true);
405
	if ($out == NULL) $out = array();
406
 
407
	if (!empty(_getResultcache())) {
408
		$stats = yt_get_playlist_stats($playlist_id);
409
		$videocount = $stats['itemCount'];
410
 
411
		$key = 'pid:'.$playlist_id;
412
 
413
		if (!isset($out[$key])) $out[$key] = array();
414
		$videocount_old = isset($out[$key]['count']) ? $out[$key]['count'] : -1;
415
	} else {
416
		$videocount = -1;
417
		$videocount_old = -2;
418
		$key = '';
419
	}
420
 
421
	if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
422
		if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
423
		$out[$key]['count'] = $videocount;
424
		$out[$key]['results'] = yt_playlist_items($playlist_id);
425
	} else {
426
		if ($verbose) echo "Video count for playlist is still $videocount, keep ".count($out[$key]['results'])." results.\n";
427
	}
428
 
429
	// Save the cache
430
 
431
	try {
432
		if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
433
	} catch(Exception $e) {
434
		fwrite(STDERR, "Cannot write result cache\n");
435
	}
436
 
437
	// Now download
438
 
6 daniel-mar 439
	if (!$out[$key]['results']) {
440
		fwrite(STDERR, "Cannot get result for playlist with ID '$playlist_id'\n");
441
		return;
442
	}
2 daniel-mar 443
	foreach ($out[$key]['results'] as list($id, $title)) {
444
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
445
		ytdwn_video_id($id);
446
	}
447
}
448
 
449
function ytdwn_search($search, $order='', $maxresults=-1) {
450
	global $type;
451
	global $verbose;
452
 
453
	if ($verbose) echo "Searching for '$search' ...\n";
454
 
455
	// Perform the search and list the videos
456
 
457
	$results = yt_search_items($search, $order, $maxresults);
458
 
459
	// Now download
460
 
6 daniel-mar 461
	if (!$results) {
462
		fwrite(STDERR, "Cannot get data for search '$search'\n");
463
		return;
464
	}
2 daniel-mar 465
	foreach ($results as list($id, $title)) {
466
		if ($verbose) echo "Downloading '$title' as ".hf_type($type)." ...\n";
467
		ytdwn_video_id($id);
468
	}
469
}
470
 
471
function ytdwn_video_id($video_id) {
472
	global $type;
473
	global $verbose;
474
	global $mp3id_transfer;
475
	global $extra_args;
476
	global $default_template;
477
	global $failTreshold;
6 daniel-mar 478
	global $cookie_file;
2 daniel-mar 479
 
480
	if (DOWNLOAD_SIMULATION_MODE) {
481
		echo "SIMULATE download of video id $video_id as ".hf_type($type)." to "._getOutputDir()."\n";
482
		return;
483
	}
484
 
485
	if (!empty(_getAlreadyDownloaded()) && in_alreadydownloaded_file($type, $video_id)) {
486
		if ($verbose) echo "Video $video_id has already been downloaded. Skip.\n";
487
		return true;
488
	}
489
 
490
	if (!empty(_getFailList()) && (ytdwn_fail_counter($type, $video_id) >= $failTreshold)) {
491
		if ($verbose) echo "Video $video_id has failed too often. Skip.\n";
492
		return true;
493
	}
494
 
495
	$out = '';
496
	$code = -1;
497
 
498
	$outputTemplate = rtrim(_getOutputDir(), '/').'/'.$default_template;
499
 
500
	if (substr($type,0,2) == 'v:') {
501
		$format = substr($type,2);
502
		if (!empty($format)) {
6 daniel-mar 503
			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 504
		} else {
6 daniel-mar 505
			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 506
		}
507
	} else if (substr($type,0,2) == 'a:') {
508
		$format = substr($type,2);
509
		if (!empty($format)) {
6 daniel-mar 510
			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 511
		} else {
6 daniel-mar 512
			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 513
		}
514
		if (($mp3id_transfer) && ($format == 'mp3')) mp3_transfer_vid_to_id();
515
	} else assert(false);
516
 
517
	if ($code == 0) {
518
		if ($verbose) fwrite(STDOUT, "Successfully downloaded video ID $video_id as ".hf_type($type)."\n");
519
		if (!empty(_getAlreadyDownloaded())) {
520
			try {
521
				addto_alreadydownloaded_file($type, $video_id);
522
			} catch(Exception $e) {
523
				fwrite(STDERR, "Cannot add to already downloaded file\n");
524
			}
525
		}
526
	} else {
527
		fwrite(STDERR, "Error downloading $video_id! (Code $code)\n");
528
		if (!empty(_getFailList())) {
529
			try {
530
				ytdwn_register_fail($type, $video_id, $code);
531
			} catch(Exception $e) {
532
				fwrite(STDERR, "Cannot register fail\n");
533
			}
534
		}
535
		return false;
536
	}
537
 
538
	return true;
539
}
540
 
541
function vid_to_vurl($video_id) {
7 daniel-mar 542
	return "https://www.youtube.com/watch?v=$video_id";
2 daniel-mar 543
}
544
 
545
function EndsWith($Haystack, $Needle){
546
	return strrpos($Haystack, $Needle) === strlen($Haystack)-strlen($Needle);
547
}
548
 
549
function mp3_transfer_vid_to_id() {
550
	global $verbose;
551
	global $default_template;
552
 
553
	if (!command_exists('id3v2')) {
554
		if ($verbose) echo "Note: Tool id3v2 is not installed. Will not transfer the YouTube ID into the MP3 ID Tag\n";
555
		return false;
556
	}
557
 
558
	if (!EndsWith($default_template, '-%(id)s.%(ext)s'))  {
559
		if ($verbose) echo "Note: Cannot transfer video tag to MP3 because default template does not end with '-%(id)s.%(ext)s'.\n";
560
		return false;
561
	}
562
 
563
	$allok = true;
564
	$files = glob(rtrim(_getOutputDir(), '/').'/*-???????????.mp3');
565
	foreach ($files as $filename) {
7 daniel-mar 566
		$m = null;
2 daniel-mar 567
		if (!preg_match('@-([a-zA-Z0-9\-_]{11})\.mp3$@ismU', $filename, $m)) continue;
568
		$yt_id = $m[1];
569
 
570
		if (!yt_check_video_id($yt_id)) continue; // just to be sure
571
 
572
		$orig_ts = filemtime($filename);
7 daniel-mar 573
		$ec = -1;
2 daniel-mar 574
		system('id3v2 -c '.escapeshellarg($yt_id).' '.escapeshellarg($filename), $ec);
575
		touch($filename, $orig_ts);
576
		if ($ec != 0) {
577
			fwrite(STDERR, "Cannot set ID tag for file $filename\n");
578
			$allok = false;
579
			continue;
580
		}
581
 
582
		$target_filename = str_replace("-$yt_id.mp3", '.mp3', $filename);
583
		if (!intelligent_rename($filename, $target_filename)) {
584
			fwrite(STDERR, "Cannot move file $filename to $target_filename\n");
585
			$allok = false;
586
			continue;
587
		}
588
	}
589
	return $allok;
590
}
591
 
592
function curl_to_cid($channel_url) {
7 daniel-mar 593
	$m = null;
8 daniel-mar 594
	if (preg_match("@https{0,1}://(www\\.|)youtube\\.com/user/(.*)(/|&|\\?)@ismU", $channel_url.'&', $m)) {
2 daniel-mar 595
		$username = $m[2];
596
		$channel_id = yt_get_channel_id($username);
597
		return $channel_id;
8 daniel-mar 598
	} else if (preg_match("@https{0,1}://(www\\.|)youtube\\.com/channel/(.*)(/|&|\\?)@ismU", $channel_url.'&', $m)) {
2 daniel-mar 599
		$channel_id = $m[2];
600
		return $channel_id;
601
	} else {
602
		return false;
603
	}
604
}
605
 
606
function in_alreadydownloaded_file($type, $video_id) {
607
	$lines = file(_getAlreadyDownloaded());
608
	foreach ($lines as $line) {
609
		if (trim($line) == rtrim($type,':').':'.$video_id) {
610
			return true;
611
		}
612
	}
613
	return false;
614
}
615
 
616
function addto_alreadydownloaded_file($type, $video_id) {
617
	file_put_contents(_getAlreadyDownloaded(), rtrim($type,':').':'.$video_id."\n", FILE_APPEND);
618
}
619
 
620
function syntax_error($msg) {
621
	fwrite(STDERR, "Syntax error: ".trim($msg)."\n");
622
	fwrite(STDERR, "Please use argument '--help' to show the syntax rules.\n");
623
	exit(2);
624
}
625
 
626
function _help() {
627
	global $argv;
628
	$out = '';
629
	$own = file_get_contents($argv[0]);
630
	$help = explode('// ----', $own, 2)[0];
7 daniel-mar 631
	$m = null;
2 daniel-mar 632
	$help = preg_match_all('@^//(.*)$@mU', $help, $m);
633
	foreach ($m[1] as $line) {
634
		$out .= substr($line,1)."\n";
635
	}
636
	return $out;
637
}
638
 
639
function help() {
640
	echo _help();
641
	exit(0);
642
}
643
 
644
function version() {
645
	echo explode("\n\n", _help(), 2)[0]."\n";
646
	exit(0);
647
}
648
 
649
function command_exists($command) {
650
	// https://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script
651
 
652
	$ec = -1;
653
	system('command -v '.escapeshellarg($command).' > /dev/null', $ec);
654
	return ($ec == 0);
655
}
656
 
657
function hf_type($type) {
658
	if (strpos($type, ':') === false) return $type; // invalid type (missing ':')
659
	list($av, $format) = explode(':', $type);
660
 
661
	if ($av == 'a') $av = 'audio';
662
	else if ($av == 'v') $av = 'video';
663
	else return $type; // invalid type
664
 
665
	return (!empty($format)) ? $format.'-'.$av : $av;
666
}
667
 
668
function expand_tilde($path) {
669
	// Source: http://jonathonhill.net/2013-09-03/tilde-expansion-in-php/
670
 
671
	if (function_exists('posix_getuid') && strpos($path, '~') !== false) {
672
		$info = posix_getpwuid(posix_getuid());
673
		$path = str_replace('~', $info['dir'], $path);
674
	}
675
 
676
	return $path;
677
}
678
 
679
function _getLastListname() {
680
	global $listFilenameStack;
681
	$listname = ''; // default
682
	if (count($listFilenameStack) > 0) {
683
		$listname = $listFilenameStack[count($listFilenameStack)-1];
684
		$listname = pathinfo($listname, PATHINFO_FILENAME); // remove file extension, e.g. ".list"
685
	}
686
	return $listname;
687
}
688
 
689
function _getApiKey() {
690
	global $apikey;
691
 
692
	$out = $apikey;
693
	if (empty($out)) {
694
		$auto_api_key = AUTO_API_KEY;
695
		$auto_api_key = expand_tilde($auto_api_key);
696
		$auto_api_key = str_replace('[listname]', _getLastListname(), $auto_api_key);
697
 
698
		if (file_exists($auto_api_key)) {
699
			$out = trim(file_get_contents($auto_api_key));
700
		} else {
701
			syntax_error("Please specify a YouTube API key with argument '-A'.");
702
		}
703
	} else {
704
		$out = str_replace('[listname]', _getLastListname(), $out);
705
		$out = expand_tilde($out);
706
 
707
		if (file_exists($out)) {
708
			$out = trim(file_get_contents($out));
709
		} else {
710
			// Assume, $out is a key, not a file
711
		}
712
	}
713
 
714
	if (!yt_check_apikey_syntax($out)) syntax_error("'$out' is not a valid API key, not an existing file containing an API key.\n");
715
 
716
	return $out;
717
}
718
 
719
function _getResultCache() {
720
	global $resultcache;
721
	if (empty($resultcache)) return '';
722
 
723
	$out = expand_tilde($resultcache);
724
 
725
	$out = str_replace('[listname]', _getLastListname(), $out);
726
 
727
	if (!file_exists($out)) {
728
		@touch($out);
729
		if (!file_exists($out)) {
730
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
731
			return '';
732
		}
733
	}
734
 
735
	return $out;
736
}
737
 
738
function _getAlreadyDownloaded() {
739
	global $alreadyDownloaded;
740
	if (empty($alreadyDownloaded)) return '';
741
 
742
	$out = expand_tilde($alreadyDownloaded);
743
 
744
	$out = str_replace('[listname]', _getLastListname(), $out);
745
 
746
	if (!file_exists($out)) {
747
		@touch($out);
748
		if (!file_exists($out)) {
749
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
750
			return '';
751
		}
752
	}
753
 
754
	return $out;
755
}
756
 
757
function _getFailList() {
758
	global $failList;
759
	if (empty($failList)) return '';
760
 
761
	$out = expand_tilde($failList);
762
 
763
	$out = str_replace('[listname]', _getLastListname(), $out);
764
 
765
	if (!file_exists($out)) {
766
		@touch($out);
767
		if (!file_exists($out)) {
768
			fwrite(STDERR, "File '$out' cannot be created. Disable feature.\n");
769
			return '';
770
		}
771
	}
772
 
773
	return $out;
774
}
775
 
776
function _getOutputDir() {
777
	global $outputDir, $allow_creation_outputdir;
778
	if (empty($outputDir)) return '.';
779
 
780
	$out = expand_tilde($outputDir);
781
 
782
	$out = str_replace('[listname]', _getLastListname(), $out);
783
 
784
	if ($allow_creation_outputdir) {
785
		if (!is_dir($out)) {
786
			mkdir($out, true);
787
			if (!is_dir($out)) {
788
				fwrite(STDERR, "Output directory '$out' does not exist.\n");
789
				exit(1);
790
			}
791
		}
792
	} else {
793
		if (!is_dir($out)) {
794
			fwrite(STDERR, "Output directory '$out' does not exist.\n");
795
			exit(1);
796
		}
797
	}
798
 
799
	return $out;
800
}
801
 
802
function parse_quoting($str) {
803
	if ((substr($str,0,1) == '"') && (substr($str,-1,1) == '"')) {
804
		$str = substr($str,1,strlen($str)-2);
805
 
806
		$escape = false;
807
		$out = '';
808
		for ($i=0; $i<strlen($str); $i++) {
809
			$char = $str[$i];
810
 
811
			if ($char == '\\') {
812
				if ($escape) {
813
					$out .= $char;
814
					$escape = false;
815
				} else {
816
					$escape = true;
817
				}
818
			} else {
819
				$out .= $char;
820
			}
821
 
822
		}
823
		$str = $out;
824
 
825
	}
826
 
827
	return $str;
828
}
829
 
830
function ytdwn_register_fail($type, $video_id, $code) {
831
	// Note: Error code $code ist currently not used
832
 
833
	$file = _getFailList();
834
	$cont = file_get_contents($file);
7 daniel-mar 835
	$m = null;
2 daniel-mar 836
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
837
		$cont = preg_replace("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU",
838
		                     "Video ID $video_id failed ".($m[1]+1)." time(s) with type $type", $cont);
839
		file_put_contents($file, $cont);
840
	} else {
841
		file_put_contents($file, "Video ID $video_id failed 1 time(s) with type $type\n", FILE_APPEND);
842
	}
843
}
844
 
845
function ytdwn_fail_counter($type, $video_id) {
846
	$file = _getFailList();
847
	$cont = file_get_contents($file);
7 daniel-mar 848
	$m = null;
2 daniel-mar 849
	if (preg_match("@Video ID ".preg_quote($video_id,'@')." failed (\d+) time\(s\) with type ".preg_quote($type,'@')."@ismU", $cont, $m)) {
850
		return $m[1];
851
	} else {
852
		return 0;
853
	}
854
}
855
 
856
function intelligent_rename($src, $dest) {
857
	$pos = strrpos($dest, '.');
858
	$ext = substr($dest, $pos);
859
	$namewoext = substr($dest, 0, $pos);
860
	$failcnt = 1;
861
	$dest_neu = $dest;
862
	while (file_exists($dest_neu)) {
863
		$failcnt++;
864
		$dest_neu = "$namewoext ($failcnt)$ext";
865
	}
866
	return rename($src, $dest_neu);
867
}
5 daniel-mar 868
 
869
function get_latest_ytdl_md5sum() {
870
	$ch = curl_init();
871
	curl_setopt($ch, CURLOPT_URL, 'https://yt-dl.org/downloads/latest/MD5SUMS');
872
	#curl_setopt($ch, CURLOPT_HEADER, false);
873
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
874
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
875
	$cont = curl_exec($ch);
7 daniel-mar 876
	$m = null;
5 daniel-mar 877
	if (preg_match('@^(.+)  youtube\-dl$@ismU', $cont, $m)) {
878
		return $m[1];
879
	} else {
880
		return false;
881
	}
882
}