Subversion Repositories yt_downloader

Rev

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