001.
using
System;
002.
using
System.Collections.Generic;
003.
using
System.ComponentModel;
004.
using
System.Configuration;
005.
using
System.Diagnostics;
006.
using
System.IO;
007.
using
System.Linq;
008.
using
System.Text.RegularExpressions;
009.
using
System.Threading;
010.
using
System.Threading.Tasks;
011.
using
NLog;
012.
013.
namespace
PlaylistDownloader
014.
{
015.
public
class
Downloader : BackgroundWorker
016.
{
017.
018.
private
readonly
ICollection<PlaylistItem> _playlist;
019.
private
readonly
RunSettings _runSettings;
020.
public
double
progressValue = 0d;
021.
private
int
_progress;
022.
private
readonly
int
_totalSongs;
023.
private
CancellationTokenSource _cts;
024.
private
static
Logger logger = LogManager.GetCurrentClassLogger();
025.
026.
public
Downloader(RunSettings settings, ICollection<PlaylistItem> playlist)
027.
{
028.
_playlist = playlist;
029.
_totalSongs = _playlist.Count;
030.
031.
_cts =
new
CancellationTokenSource();
032.
033.
_runSettings = settings;
034.
}
035.
036.
protected
override
void
OnDoWork(DoWorkEventArgs args)
037.
{
038.
_progress = 0;
039.
040.
041.
ParallelOptions po =
new
ParallelOptions
042.
{
043.
CancellationToken = _cts.Token,
044.
MaxDegreeOfParallelism = Environment.ProcessorCount
045.
};
046.
047.
try
048.
{
049.
050.
try
051.
{
052.
Parallel.ForEach(_playlist, po, async item =>
053.
{
054.
try
055.
{
056.
po.CancellationToken.ThrowIfCancellationRequested();
057.
await DownloadPlaylistItem(item);
058.
}
059.
catch
(InvalidOperationException) { }
060.
catch
(Win32Exception) { }
061.
});
062.
}
063.
catch
(OperationCanceledException) { }
064.
finally
065.
{
066.
if
(_cts !=
null
)
067.
{
068.
_cts.Dispose();
069.
_cts =
null
;
070.
}
071.
}
072.
073.
074.
075.
076.
077.
078.
079.
080.
081.
082.
083.
}
084.
catch
(OperationCanceledException) { }
085.
}
086.
087.
public
async Task<
string
> DownloadPlaylistItem(PlaylistItem item)
088.
{
089.
string
destinationFilePathWithoutExtension =
null
;
090.
string
tempFilePathWithoutExtension =
null
;
091.
092.
item.DownloadProgress = 5;
093.
if
(!
string
.IsNullOrWhiteSpace(item.FileName))
094.
{
095.
096.
097.
var workingFolder = Path.GetTempPath();
098.
tempFilePathWithoutExtension = Path.Combine(Path.GetTempPath(), item.FileName);
099.
destinationFilePathWithoutExtension = Path.Combine(_runSettings.SongsFolder, item.FileName);
100.
101.
if
(!File.Exists(destinationFilePathWithoutExtension +
".mp3"
))
102.
{
103.
item.DownloadProgress = 10;
104.
105.
106.
107.
await StartProcess(
108.
_runSettings.YoutubeDlPath,
109.
string
.Format(
" --ffmpeg-location \"{0}\""
+
110.
" --format bestaudio[ext=mp3]/best"
+
111.
" --audio-quality 0"
+
112.
" --no-part"
+
113.
" --extract-audio"
+
114.
" --audio-format mp3"
+
115.
" --output \"{1}\""
+
116.
" {2}"
, _runSettings.FfmpegPath, tempFilePathWithoutExtension +
"-raw.%(ext)s"
, item.Link),
117.
item,
118.
ParseYoutubeDlProgress);
119.
120.
121.
122.
await StartProcess(_runSettings.FfmpegPath,
123.
string
.Format(
" -i \"{0}\""
+
124.
" -af loudnorm=I=-16:TP=-1.5:LRA=11"
+
125.
126.
" -y"
+
127.
" \"{1}\""
, tempFilePathWithoutExtension +
"-raw.mp3"
, tempFilePathWithoutExtension + _runSettings.NormalizedSuffix +
".mp3"
),
128.
item,
129.
ParseYoutubeDlProgress);
130.
131.
132.
File.Move(tempFilePathWithoutExtension + _runSettings.NormalizedSuffix +
".mp3"
,
133.
destinationFilePathWithoutExtension + _runSettings.NormalizedSuffix +
".mp3"
);
134.
135.
136.
File.Delete(Path.Combine(_runSettings.SongsFolder, item.FileName +
"-raw.mp3"
));
137.
138.
139.
140.
}
141.
}
142.
143.
item.DownloadProgress = 100;
144.
_progress++;
145.
OnProgressChanged(
new
ProgressChangedEventArgs(_progress * 100 / _totalSongs,
null
));
146.
progressValue = Convert.ToDouble(((
double
)_progress * 100d / (
double
)_totalSongs).ToString(
"N2"
));
147.
148.
149.
return
destinationFilePathWithoutExtension;
150.
}
151.
152.
private
void
ParseYoutubeDlProgress(
string
consoleLine, PlaylistItem item)
153.
{
154.
155.
Regex extractDownloadProgress =
new
Regex(@
"\[download\][\s]*([0-9\.]+)%"
);
156.
Match match = extractDownloadProgress.Match(consoleLine);
157.
if
(match.Length > 0 && match.Groups.Count >= 2)
158.
{
159.
if
(
double
.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture,
out
double
downloadProgress))
160.
{
161.
logger.Info(
"[download + convert progress] "
+ downloadProgress);
162.
if
(downloadProgress > 100 || _progress > 100)
163.
{
164.
Debugger.Break();
165.
}
166.
item.DownloadProgress = (
int
)(10.0 + downloadProgress / 100 * 60);
167.
OnProgressChanged(
new
ProgressChangedEventArgs(_progress * 100 / _totalSongs,
null
));
168.
}
169.
}
170.
}
171.
172.
private
void
ParseNormalizeProgress(
string
consoleLine, PlaylistItem item)
173.
{
174.
175.
176.
Regex extractDuration =
new
Regex(@
"Duration:\s*([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{2}),\sstart:"
);
177.
Match match = extractDuration.Match(consoleLine);
178.
if
(match.Length > 0 && match.Groups.Count >= 2)
179.
{
180.
logger.Info(
"Duration: "
+ match.Groups[0].Value);
181.
item.Duration = TimeSpan.Parse(match.Groups[0].Value).TotalSeconds;
182.
logger.Info(
"Duration seconds: "
+ item.Duration);
183.
return
;
184.
}
185.
186.
if
(item.Duration == 0)
187.
{
188.
return
;
189.
}
190.
191.
Regex extractProgressDuration =
new
Regex(@
"time=([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{2})\s*bitrate="
);
192.
match = extractProgressDuration.Match(consoleLine);
193.
if
(match.Length > 0 && match.Groups.Count >= 2)
194.
{
195.
logger.Info(
"progress Duration: "
+ match.Groups[0].Value);
196.
logger.Info(
"progress Duration seconds: "
+ TimeSpan.Parse(match.Groups[0].Value).TotalSeconds);
197.
item.DownloadProgress = (
int
)(70 + TimeSpan.Parse(match.Groups[0].Value).TotalSeconds / item.Duration * 30);
198.
OnProgressChanged(
new
ProgressChangedEventArgs(_progress * 100 / _totalSongs,
null
));
199.
}
200.
}
201.
202.
private
Task<
string
> StartProcess(
string
executablePath,
string
arguments, PlaylistItem item, Action<
string
, PlaylistItem> parseProgressFunc)
203.
{
204.
var promise =
new
TaskCompletionSource<
string
>();
205.
logger.Info(
"[RUN CMD] "
+ executablePath + arguments);
206.
Process process =
new
Process
207.
{
208.
StartInfo =
209.
{
210.
FileName = executablePath,
211.
Arguments = arguments,
212.
CreateNoWindow = !_runSettings.IsDebug,
213.
WindowStyle = _runSettings.IsDebug ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
214.
RedirectStandardOutput =
true
,
215.
RedirectStandardError =
true
,
216.
UseShellExecute =
false
217.
218.
},
219.
EnableRaisingEvents =
true
220.
};
221.
222.
223.
process.OutputDataReceived += (
object
sender, DataReceivedEventArgs e) =>
224.
{
225.
string
consoleLine = e.Data;
226.
if
(!
string
.IsNullOrWhiteSpace(consoleLine))
227.
{
228.
logger.Info(consoleLine);
229.
parseProgressFunc(consoleLine, item);
230.
}
231.
232.
if
(CancellationPending)
233.
{
234.
logger.Info(
"Canceling process because of user: "
+ executablePath);
235.
process.Close();
236.
promise.SetResult(
null
);
237.
}
238.
};
239.
240.
process.ErrorDataReceived += (
object
sender, DataReceivedEventArgs e) =>
241.
{
242.
string
consoleLine = e.Data;
243.
244.
if
(!
string
.IsNullOrWhiteSpace(consoleLine))
245.
{
246.
logger.Info(
"Error: "
+ consoleLine);
247.
parseProgressFunc(consoleLine, item);
248.
}
249.
250.
if
(CancellationPending)
251.
{
252.
logger.Info(
"Canceling process because of user: "
+ executablePath);
253.
process.Close();
254.
promise.SetResult(
null
);
255.
}
256.
};
257.
258.
process.Exited +=
new
EventHandler((
object
sender, EventArgs e) =>
259.
{
260.
process.Dispose();
261.
logger.Info(
"Closing process"
);
262.
promise.SetResult(
null
);
263.
});
264.
265.
process.Start();
266.
267.
process.BeginOutputReadLine();
268.
process.BeginErrorReadLine();
269.
270.
return
promise.Task;
271.
}
272.
273.
internal
void
Abort()
274.
{
275.
if
(_cts !=
null
)
276.
{
277.
_cts.Cancel();
278.
_cts =
null
;
279.
}
280.
CancelAsync();
281.
}
282.
283.
private
static
string
MakeValidFileName(
string
name)
284.
{
285.
return
Regex.Replace(
286.
name,
287.
"[^A-Za-z0-9_ -]+"
,
288.
"-"
,
289.
RegexOptions.IgnoreCase).Trim(
'-'
,
' '
).ToLower();
290.
}
291.
}
292.
}