Subversion Repositories yt_downloader

Compare Revisions

No changes between revisions

Regard whitespace Rev 15 → Rev 16

/trunk/README.md
1,31 → 1,44
 
# ViaThinkSoft YouTube Downloader Util 2.3
# ViaThinkSoft YouTube Downloader 2.3.1
 
YouTube Downloader is a tool for Linux. It is a wrapper for youtube-dl (or any compatible forks) and offers several additional functionalities.
 
Special features:
- Downloading of all videos of a channel or a playlist.
- Automatic searching inside channels or globally (whole YouTube)
- You can download videos and audio files.
- YouTube-IDs can be automatically written in the ID tag of downloaded mp3 files.
- An automatically managed list of already downloaded videos allows you to move away from the downloaded files without the risk of downloading the already downloaded files again.
- An automatically managed list of failed downloads will avoid that a video, which is not available anymore, is tried to be downloaded too many times.
- Creation of SFV and/or MD5 checksum files.
- The tool is fully CLI and is optimized for cronjobs.
 
## Syntax
 
./ytdwn [-t|--type v:[ext]|a:[ext]] (default v:)
[-o|--outputDir <dir>] (default current working directory)
./ytdwn [-t|--type v:[ext]|a:[ext]] Type video or audio and preferred output type, e.g. 'a:mp3' (default v:)
[-o|--outputDir <dir>] Default current working directory
[-a|--alreadyDownloaded <file>]
[-f|--failList <file> <treshold>] (This file logs failures)
[-F|--failTreshold <num>] (Don't download if failure (-f) treshold is reached. Default: 3)
[-V|--version] (shows version)
[-v|--verbose] (displays verbose information to STDOUT)
[-h|--help] (shows help)
[-N|--no-mp3-tagtransfer] (disables transfer of video ID to MP3 ID tag)
(This feature requires the package "id3v2")
[-H|--checksumMode] (Which checksum files shall be written for new files.
Must be 'None', 'MD5', 'SFV', or 'MD5,SFV')
[-T|--default-template <t>] (Sets default filename template.)
[-f|--failList <file> <treshold>] This file logs failures.
[-F|--failTreshold <num>] Don't download if failure (-f) treshold is reached. (Default: 3)
[-V|--version] Shows the version
[-v|--verbose] Displays verbose information to STDOUT
[-h|--help] Shows the help page.
[-N|--no-mp3-tagtransfer] Disables transfer of video ID to MP3 ID tag.
This feature requires the package "id3v2".
[-H|--checksumMode] Which checksum files shall be written for new files.
Must be 'None', 'MD5', 'SFV', or 'MD5,SFV'.
[-T|--default-template <t>] Sets default filename template.
(Default: '%(title)s-%(id)s.%(ext)s')
[-X|--extra-args <args>] (Additional arguments passed through)
(youtube-dl. Default "-ic")
[-A|--api-key <file|key>] (specifies the API key, or a file containing the API key)
[-X|--extra-args <args>] Additional arguments passed through youtube-dl. (Default "-ic")
[-A|--api-key <file|key>] Specifies the API key, or a file containing the API key
(Default: ~/.yt_api_key)
[--cookies=<file>] A netscape compatible cookie file (for age restricted videos)
[--cookies=<file>] A netscape compatible cookie file (for age restricted videos
(Default: ~/.yt_cookkies)
[-C|--resultcache <file>] (allows video results to be cached in this file)
(only for playlists or channels)
[-O|--create-outputdir] (allows creation of the output directories, recursively)
[-C|--resultcache <file>] Allows video results to be cached in this file;
only for playlists or channels.
[-O|--create-outputdir] Allows creation of the output directories, recursively.
[--downloader=exename] Binary file name of a youtube-dl compatible tool.
Currently supported/tested: youtube-dl, youtube-dlc, yt-dlp.
[--]
<resource> [<resource> ...]
 
69,7 → 82,6
 
## Requirements
- PHP CLI
- Package "youtube-dl" (ytdwn will try to download it automatically, if possible)
- A YouTube API key (can be obtained here: https://console.developers.google.com/apis/credentials )
- If you want to extract audio, you need additionally: ffmpeg or avconv and ffprobe or avprobe.
- Optional: package "id3v2" to allow the YouTube video id to be transferred to the MP3 ID tag
/trunk/youtube_functions.inc.phps
43,8 → 43,8
$next_page_token = '';
 
do {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId='.urlencode($playlist_id).'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
if (!$cont) return false;
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId='.urlencode($playlist_id).'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
if (!$cont) return false; // e.g. if Playlist was deleted
 
$obj = json_decode($cont, true);
if (!$obj) return false;
67,7 → 67,7
}
 
function yt_get_channel_id($username) {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&forUsername='.urlencode($username).'&part=id');
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&forUsername='.urlencode($username).'&part=id');
if (!$cont) return false;
 
$obj = json_decode($cont, true);
83,7 → 83,7
}
 
function yt_get_channel_id_and_stats($username) {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&forUsername='.urlencode($username).'&part=id,statistics');
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&forUsername='.urlencode($username).'&part=id,statistics');
if (!$cont) return false;
 
$obj = json_decode($cont, true);
99,7 → 99,7
}
 
function yt_get_channel_stats($channel_id) {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&id='.urlencode($channel_id).'&part=statistics');
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/channels?key='.urlencode(yt_get_apikey()).'&id='.urlencode($channel_id).'&part=statistics');
if (!$cont) return false;
 
$obj = json_decode($cont, true);
115,7 → 115,7
}
 
function yt_get_playlist_stats($playlist_id) {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/playlists?part=contentDetails&id='.urlencode($playlist_id).'&key='.urlencode(yt_get_apikey()));
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/playlists?part=contentDetails&id='.urlencode($playlist_id).'&key='.urlencode(yt_get_apikey()));
if (!$cont) return false;
 
$obj = json_decode($cont, true);
125,6 → 125,7
 
foreach ($obj['items'] as $item) {
if ($item['kind'] == 'youtube#playlist') {
if (!isset($item['contentDetails']) || is_null($item['contentDetails'])) return false; // can happen to deleted playlists
return $item['contentDetails'];
}
}
136,7 → 137,7
$next_page_token = '';
 
do {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/search?part=snippet&channelId='.urlencode($channel_id).(($searchterms!='') ? '&q='.urlencode($searchterms) : '').'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/search?part=snippet&channelId='.urlencode($channel_id).(($searchterms!='') ? '&q='.urlencode($searchterms) : '').'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
if (!$cont) return false;
 
$obj = json_decode($cont, true);
166,7 → 167,7
$next_page_token = '';
 
do {
$cont = file_get_contents('https://www.googleapis.com/youtube/v3/search?part=snippet&q='.urlencode($searchterms).(($order!='') ? '&order='.urlencode($order) : '').'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
$cont = @file_get_contents('https://www.googleapis.com/youtube/v3/search?part=snippet&q='.urlencode($searchterms).(($order!='') ? '&order='.urlencode($order) : '').'&maxResults=50'.(($next_page_token!='') ? '&pageToken='.urlencode($next_page_token) : '').'&key='.urlencode(yt_get_apikey()));
if (!$cont) return false;
 
$obj = json_decode($cont, true);
241,7 → 242,7
// https://www.youtube.com/impaulsive
// <link rel="canonical" href="https://www.youtube.com/channel/UCGeBogGDZ9W3dsGx-mWQGJA">
 
$cont = file_get_contents($custom_url);
$cont = @file_get_contents($custom_url);
if ($cont === false) {
throw new Exception("Cannot open $custom_url using file_get_contents.");
}
/trunk/ytdwn
1,7 → 1,7
#!/usr/bin/php
<?php
 
// ViaThinkSoft YouTube Downloader Util 2.3
// ViaThinkSoft YouTube Downloader Util 2.3.1
// Revision: 2022-02-07
// Author: Daniel Marschall <www.daniel-marschall.de>
// Licensed under the terms of the Apache 2.0 License
54,6 → 54,7
'-c '; // resume partially downloaded video files
$default_template = '%(title)s-%(id)s.%(ext)s';
$cookie_file = AUTO_COOKIE_FILE;
$downloader = 'yt-dlp';
 
// Parse arguments
// 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
110,6 → 111,10
array_shift($argv_bak);
if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
$cookie_file = file_exists($m[3]) ? trim(file_get_contents($m[3])) : $m[3];
} else if (preg_match('@^(\-\-downloader)(\s+|=)(.*)$@s', $arg2, $m)) {
array_shift($argv_bak);
if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
$downloader = $m[3];
} else if (preg_match('@^(/X|\-X|\-\-extra-args)(\s+|=)(.*)$@s', $arg2, $m)) {
array_shift($argv_bak);
if (count($rest_args) > 0) syntax_error("Invalid argument: ".$rest_args[0]);
146,6 → 151,8
unset($argv_bak);
unset($init_extra_args);
 
define('DOWNLOAD_YT_FORK', $downloader);
 
// Validity checks
 
if ((substr($type,0,2) != 'a:') && (substr($type,0,2) != 'v:')) syntax_error("Type must be either 'v:' or 'a:'. '$type' is not valid.");
157,45 → 164,19
$cookie_file = expand_tilde($cookie_file);
if (!file_exists($cookie_file)) $cookie_file = '';
 
// Try to download/update youtube-dl into local directory
// Try to download/update youtube-dl/yt-dlp into local directory
download_latest_downloader();
 
$newest_version_md5 = get_latest_ytdl_md5sum();
if (!$newest_version_md5) {
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");
if (command_exists(__DIR__.'/'.DOWNLOAD_YT_FORK)) {
echo "Will use '".DOWNLOAD_YT_FORK."' from local directory\n";
define('YTDL_EXE', __DIR__.'/'.DOWNLOAD_YT_FORK);
} else {
if (!file_exists(__DIR__.'/youtube-dl') || ($newest_version_md5 != md5_file(__DIR__.'/youtube-dl'))) {
// Try to download/update the file in our directory. It should be the newest available, since YT often breaks downloader
if (file_exists(__DIR__.'/youtube-dl')) {
echo "Trying to update 'youtube-dl' in local directory...\n";
} else {
echo "Trying to download 'youtube-dl' into local directory...\n";
}
 
@chmod(__DIR__.'/youtube-dl', 0777); // otherwise we might not be able to write to it
 
if (!($binary = file_get_contents('https://yt-dl.org/latest/youtube-dl'))) {
fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (file_get_contents).\n");
} else if (!@file_put_contents(__DIR__.'/youtube-dl', $binary)) {
fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (file_put_contents).\n");
} else {
if (!@chmod(__DIR__.'/youtube-dl', 0544)) {
fwrite(STDERR, "Failed to download 'youtube-dl' into local directory (chmod 544).\n");
@unlink(__DIR__.'/youtube-dl'); // try to delete, otherwise we might try to execute a non-executable file
}
}
}
}
 
if (command_exists(__DIR__.'/youtube-dl')) {
echo "Will use 'youtube-dl' from local directory\n";
define('YTDL_EXE', __DIR__.'/youtube-dl');
} else {
// Download failed. Is at least a package installed?
if (command_exists('youtube-dl')) {
echo "Will use 'youtube-dl' from Linux package\n";
define('YTDL_EXE', 'youtube-dl');
if (command_exists(DOWNLOAD_YT_FORK)) {
echo "Will use '".DOWNLOAD_YT_FORK."' from Linux package\n";
define('YTDL_EXE', DOWNLOAD_YT_FORK);
} else {
fwrite(STDERR, "This script requires the tool/package 'youtube-dl'. Please install it first.\n");
fwrite(STDERR, "This script requires the tool/package '".DOWNLOAD_YT_FORK."'. Please install it first.\n");
exit(1);
}
}
357,11 → 338,12
 
// List the videos of the channel
 
$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
$use_cache = !empty(_getResultcache()) && file_exists(_getResultcache());
$cont = $use_cache ? file_get_contents(_getResultcache()) : '';
$out = json_decode($cont, true);
if ($out == NULL) $out = array();
 
if (!empty(_getResultcache())) {
if ($use_cache) {
$stats = yt_get_channel_stats($channel_id);
if ($stats === false) {
fwrite(STDERR, "Cannot get stats for channel with ID '$channel_id'\n");
380,7 → 362,7
}
 
if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
if ($verbose && $use_cache) echo "Video count changed from $videocount_old to $videocount\n";
$out[$key]['count'] = $videocount;
if (!empty($search)) {
$out[$key]['results'] = yt_channel_items($channel_id, $search);
387,6 → 369,10
} else {
$out[$key]['results'] = yt_channel_items($channel_id);
}
if (!$out[$key]['results']) {
fwrite(STDERR, "Cannot get result for channel with ID '$channel_id'\n");
return;
}
} else {
if ($verbose) echo "Video count for channel is still $videocount, keep ".count($out[$key]['results'])." results.\n";
}
394,7 → 380,7
// Save the cache
 
try {
if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
if ($use_cache) file_put_contents(_getResultcache(), json_encode($out));
} catch(Exception $e) {
fwrite(STDERR, "Cannot write result cache\n");
}
419,11 → 405,12
 
// List the videos of the playlist
 
$cont = !empty(_getResultcache()) && file_exists(_getResultcache()) ? file_get_contents(_getResultcache()) : '';
$use_cache = !empty(_getResultcache()) && file_exists(_getResultcache());
$cont = $use_cache ? file_get_contents(_getResultcache()) : '';
$out = json_decode($cont, true);
if ($out == NULL) $out = array();
 
if (!empty(_getResultcache())) {
if ($use_cache) {
$stats = yt_get_playlist_stats($playlist_id);
if ($stats === false) {
fwrite(STDERR, "Cannot get stats for playlist with ID '$playlist_id'\n");
442,9 → 429,13
}
 
if ($videocount_old != $videocount) { // Attention: This might not work if videos are deleted and added (= the count stays the same)
if ($verbose && !empty(_getResultcache())) echo "Video count changed from $videocount_old to $videocount\n";
if ($verbose && $use_cache) echo "Video count changed from $videocount_old to $videocount\n";
$out[$key]['count'] = $videocount;
$out[$key]['results'] = yt_playlist_items($playlist_id);
if (!$out[$key]['results']) {
fwrite(STDERR, "Cannot get result for playlist with ID '$playlist_id'\n");
return;
}
} else {
if ($verbose) echo "Video count for playlist is still $videocount, keep ".count($out[$key]['results'])." results.\n";
}
452,7 → 443,7
// Save the cache
 
try {
if (!empty(_getResultcache())) file_put_contents(_getResultcache(), json_encode($out));
if ($use_cache) file_put_contents(_getResultcache(), json_encode($out));
} catch(Exception $e) {
fwrite(STDERR, "Cannot write result cache\n");
}
930,8 → 921,69
return $res;
}
 
function get_latest_ytdl_md5sum() {
function download_latest_downloader() {
$download_url = false;
 
if (DOWNLOAD_YT_FORK == 'yt-dlp') {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS');
#curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$cont = curl_exec($ch);
$m = null;
if (preg_match('@^(.+)\s+yt-dlp$@ismU', $cont, $m)) {
$newest_version_sha2 = $m[1];
} else {
$newest_version_sha2 = false;
}
 
if (!$newest_version_sha2) {
fwrite(STDERR, "Failed to get SHA2 sum of latest version of '".DOWNLOAD_YT_FORK."' online. Will not try to download/update '".DOWNLOAD_YT_FORK."' into local directory.\n");
} else {
if (!file_exists(__DIR__.'/'.DOWNLOAD_YT_FORK) || ($newest_version_sha2 != hash_file('sha256',__DIR__.'/'.DOWNLOAD_YT_FORK))) {
$download_url = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
}
}
}
 
if (DOWNLOAD_YT_FORK == 'youtube-dlc') {
// TODO: What is the difference between these two? Which one should be chosen?!
// https://github.com/blackjack4494/yt-dlc (18372 commits, last commit 23 Aug 2021, last release 2020.11.11-3)
// https://github.com/blackjack4494/youtube-dlc (18888 commits, last commit 26 Jul 2021, last release 2020.10.09)
 
// TODO: yt-dlc The sha256 does not fit to the binary! Therefore, it is always downloaded!
// TODO: youtube-dlc has no hashes online...
/*
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/SHA2-256SUMS');
#curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$cont = curl_exec($ch);
$m = null;
if (preg_match('@^youtube\-dlc:(.+)$@ismU', $cont, $m)) {
$newest_version_sha2 = $m[1];
} else {
$newest_version_sha2 = false;
}
 
if (!$newest_version_sha2) {
fwrite(STDERR, "Failed to get SHA2 sum of latest version of '".DOWNLOAD_YT_FORK."' online. Will not try to download/update '".DOWNLOAD_YT_FORK."' into local directory.\n");
} else {
if (!file_exists(__DIR__.'/'.DOWNLOAD_YT_FORK) || ($newest_version_sha2 != hash_file('sha256',__DIR__.'/'.DOWNLOAD_YT_FORK))) {
$download_url = 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc';
}
}
*/
 
if (!file_exists(__DIR__.'/'.DOWNLOAD_YT_FORK) || (time()-filemtime(__DIR__.'/'.DOWNLOAD_YT_FORK) > 24*60*60)) {
$download_url = 'https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc';
}
}
 
if (DOWNLOAD_YT_FORK == 'youtube-dl') {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://yt-dl.org/downloads/latest/MD5SUMS');
#curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
939,8 → 991,39
$cont = curl_exec($ch);
$m = null;
if (preg_match('@^(.+) youtube\-dl$@ismU', $cont, $m)) {
return $m[1];
$newest_version_md5 = $m[1];
} else {
return false;
$newest_version_md5 = false;
}
 
if (!$newest_version_md5) {
fwrite(STDERR, "Failed to get MD5 sum of latest version of '".DOWNLOAD_YT_FORK."' online. Will not try to download/update '".DOWNLOAD_YT_FORK."' into local directory.\n");
} else {
if (!file_exists(__DIR__.'/'.DOWNLOAD_YT_FORK) || ($newest_version_md5 != md5_file(__DIR__.'/'.DOWNLOAD_YT_FORK))) {
$download_url = 'https://yt-dl.org/latest/youtube-dl';
}
}
}
 
if ($download_url !== false) {
// Try to download/update the file in our directory. It should be the newest available, since YT often breaks downloader
if (file_exists(__DIR__.'/'.DOWNLOAD_YT_FORK)) {
echo "Trying to update '".DOWNLOAD_YT_FORK."' in local directory...\n";
} else {
echo "Trying to download '".DOWNLOAD_YT_FORK."' into local directory...\n";
}
 
@chmod(__DIR__.'/'.DOWNLOAD_YT_FORK, 0777); // otherwise we might not be able to write to it
 
if (!($binary = file_get_contents($download_url))) {
fwrite(STDERR, "Failed to download '".DOWNLOAD_YT_FORK."' into local directory (file_get_contents).\n");
} else if (!@file_put_contents(__DIR__.'/'.DOWNLOAD_YT_FORK, $binary)) {
fwrite(STDERR, "Failed to download '".DOWNLOAD_YT_FORK."' into local directory (file_put_contents).\n");
} else {
if (!@chmod(__DIR__.'/'.DOWNLOAD_YT_FORK, 0544)) {
fwrite(STDERR, "Failed to download '".DOWNLOAD_YT_FORK."' into local directory (chmod 544).\n");
@unlink(__DIR__.'/'.DOWNLOAD_YT_FORK); // try to delete, otherwise we might try to execute a non-executable file
}
}
}
}
/trunk/.
Property changes:
Modified: svn:ignore
marschall
+test
ffmpeg
youtube-dl
+youtube-dlc
+yt-dlp
upload_to_vts
phpstan.neon
.phpstan.tmp