Subversion Repositories yt_downloader

Rev

Rev 5 | Go to most recent revision | Details | Last modification | View Log | RSS feed

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