-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathytdl.d
183 lines (160 loc) · 4.85 KB
/
ytdl.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
* ytdl.d: helper functions for downloading YouTube videos
*
* The GPLv2 License (GPLv2)
* Copyright (c) 2023 Jeremy Baxter
*
* ytunnel is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* ytunnel is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ytunnel. If not, see <http://www.gnu.org/licenses/>.
*/
module ytdl;
import std.algorithm : all, canFind, findSplitBefore, startsWith;
import std.exception : basicExceptionCtors, enforce;
/**
* Exception thrown when failure of downloading or
* converting YouTube media occurs.
*/
class YouTubeDownloadException : Exception
{
mixin basicExceptionCtors;
}
/**
* Returns true if the given string is
* a valid YouTube video ID.
*/
bool
validYouTubeVideoID(in char[] id) @safe pure
{
import std.ascii : letters, digits;
string validCharacters = letters ~ digits ~ "-_";
if (id.length != 11)
return false;
return all!(ch => validCharacters.canFind(ch))(id);
}
/**
* Separates a YouTube video URL from its ID.
* Works with youtube.com/watch?v= and youtu.be.
* If the URL isn't detected to be a youtube.com/watch?v
* URL or a youtu.be URL, the original URL is returned
* (in case only the video ID is given).
*
* Example:
* separateYouTubeID("https://www.youtube.com/watch?v=H3inzGGFefg")
* -> "H3inzGGFefg"
*/
char[]
separateYouTubeID(in char[] url) @safe pure
{
bool
startsWithHTTPS(in char[] url, string mdl) @safe pure
{
return startsWith(url,
"http://" ~ mdl,
"https://" ~ mdl,
"http://www." ~ mdl,
"https://www." ~ mdl) != 0;
}
char[]
stripURL(in char[] url, string prefix) @safe pure
{
import std.string : stripLeft;
return url
.stripLeft("http")
.stripLeft("s")
.stripLeft("://")
.stripLeft("www.")
.stripLeft(prefix)
.findSplitBefore("?")[0]
.dup();
}
return
(startsWithHTTPS(url, "youtu.be/") ?
stripURL(url, "youtu.be/") :
startsWithHTTPS(url, "youtube.com/watch?v=") ?
stripURL(url, "youtube.com/watch?v=")
: url).dup();
}
/**
* Makes a request to youtube.com and returns
* the given video ID's title as a string.
*/
string
youTubeVideoTitle(in char[] id) @trusted
in (validYouTubeVideoID(id), "Invalid video ID")
{
import std.json : parseJSON;
import std.net.curl : get;
return get(
"https://www.youtube.com/oembed?format=json&url=http%3A//youtube.com/watch%3Fv%3D"
~ id).parseJSON()["title"].str;
}
/**
* Using yt-dlp, downloads the given YouTube video
* identified by id into a temporary file, and converts
* it using ffmpeg to the format indicated by the
* extension of the file name dest.
*
* fmt is a string that describes the audio/video
* format to output. A list of all possible options
* is provided in the manual page under "FORMAT SELECTION",
* or you can pass the -F flag to see specific formats
* for a particular video. If fmt is null, uses the default
* setting for yt-dlp (bestvideo*+bestaudio/best).
*
* After downloading is complete, the media will be
* converted using ffmpeg into the file extension
* specified by dest, e.g. a dest file of song.mp3
* will convert the content into that of an mp3 format.
*
* Throws ProcessException if failure to start a process
* (yt-dlp or ffmpeg) occurs, FileException if traversing
* the current directory fails somehow, or YouTubeDownloadException
* if one of the started processes returns a non-zero exit code.
*/
void
downloadYouTubeVideo(string id, scope const(char[]) fmt, string dest) @trusted
in (validYouTubeVideoID(id), "Invalid video ID")
{
import std.file : dirEntries, remove, rename, DirEntry, SpanMode;
string yTmp;
void
spawn(scope const(char[])[] args)
{
import std.conv : to;
import std.process : spawnProcess, wait;
int status;
status = spawnProcess(args).wait();
enforce!YouTubeDownloadException(status == 0,
args[0] ~ " failed with exit code " ~ status.to!string());
}
yTmp = id ~ ".ytmp";
spawn(["yt-dlp", "-q",
"-f", fmt == null ? "bestvideo*+bestaudio/best" : fmt,
"-o", yTmp, "--", id]);
/*
* yt-dlp can sometimes create output files with names that
* have an extra extension on the end; this code looks for
* a file (not a directory) that begins with the specified
* output file name, and renames it to the intended output
* file.
*/
foreach (DirEntry ent; dirEntries(".", yTmp ~ ".*", SpanMode.shallow)) {
if (!ent.isDir && ent.isFile) {
rename(ent.name, yTmp);
break;
}
}
spawn(["ffmpeg", "-hide_banner", "-loglevel", "error",
"-y", "-i", yTmp, "file:" ~ dest]);
remove(yTmp);
}