├── .github
└── FUNDING.yml
├── CHANGELOG.md
├── README.md
├── _config.yml
├── _updated.log
├── screenshot.png
└── source
├── Cache.cs
├── Constants.cs
├── Preparer.cs
├── Program.cs
├── Saver.cs
├── Validator.cs
└── Waiter.cs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: payoneer
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [24.061] - 2024-06-22
4 |
5 | ### Fixed
6 |
7 | - Minor fix.
8 |
9 | ## [23.081] - 2023-08-04
10 |
11 | ### Fixed
12 |
13 | - Minor fix.
14 |
15 | ## [23.071] - 2023-07-25
16 |
17 | ### Fixed
18 |
19 | - Fixed YouTube throttling. For faster downloading, use Chromium-based browsers. Attention: updated browsers are not compatible, for example, Chrome only works with version 110 and below. AVC video data is also not available, only VP9.
20 |
21 | ## [22.122] - 2022-12-19
22 |
23 | ### Fixed
24 |
25 | - Several minor fixes.
26 |
27 | ## [22.121] - 2022-12-03
28 |
29 | ### Changed
30 |
31 | - Changed the behavior of `-browser` parameter. See its description for more information.
32 |
33 | ### Fixed
34 |
35 | - Several minor fixes.
36 |
37 | ## [22.062] - 2022-06-22
38 |
39 | ### Fixed
40 |
41 | - Several minor fixes.
42 |
43 | ## [22.061] - 2022-06-06
44 |
45 | ### Added
46 |
47 | - The ability to cache technical information about the live stream. Read about the `-keepstreaminfo` parameter to not use the cache.
48 |
49 | ### Changed
50 |
51 | - Improved and documented `-log` parameter.
52 |
53 | ### Fixed
54 |
55 | - Several minor fixes.
56 |
57 | ## [22.051] - 2022-05-19
58 |
59 | ### Added
60 |
61 | - Downloading of recently finished live streams (no more than 6 hours ago).
62 | - Option `-duration=[minutes].[seconds]`, for better usability.
63 | - Option `-resolution=0` to save audio only (works with both audio and video formats except `.mp4`).
64 | - Options `-duration=max`, `-duration=min`, `-resolution=max`, `-resolution=min`, for better usability.
65 | - Saving to `.m3u` and `.m3u8` formats (allows to play the specified part of the live stream without downloading, only tested with VLC mediaplayer).
66 |
67 | ### Changed
68 |
69 | - Default media container format, from `.mp4` to `.mkv`.
70 |
71 | ### Fixed
72 |
73 | - Several minor fixes.
74 |
75 | ### Removed
76 |
77 | - Option `-start=wait` (now the program is waiting for live streams without this option).
78 |
79 | ## [22.041] - 2022-04-14
80 |
81 | ### Added
82 |
83 | - Parameter `-browser` to allow browser to be used (in headless mode) if Yrewind can't get live stream info on its own.
84 |
85 | ### Changed
86 |
87 | - Now the browser is used only if the `-browser` parameter is specified.
88 |
89 | ### Fixed
90 |
91 | - Error when saving some live streams.
92 | - Several minor fixes.
93 |
94 | ## [22.011] - 2022-01-21
95 |
96 | ### Fixed
97 |
98 | - Several minor fixes.
99 |
100 | ## [21.121] - 2021-12-01
101 |
102 | ### Changed
103 |
104 | - Nothing has changed (technical release).
105 |
106 | ## [21.063] - 2021-06-20
107 |
108 | ### Changed
109 |
110 | - The way to get technical information about live stream - Yrewind now uses a browser (Google Chrome or Microsoft Edge if Chrome is not installed). This method is slightly slower and more buggy, but more resilient to various changes on YouTube servers.
111 |
112 | ## [21.062] - 2021-06-15
113 |
114 | ### Fixed
115 |
116 | - Several minor fixes.
117 |
118 | ## [21.061] - 2021-06-03
119 |
120 | ### Added
121 |
122 | - Parameter `-cookie` to save live stream using cookie file.
123 |
124 | ### Changed
125 |
126 | - The `-start` parameter is now calculated not relative to the time on the computer, but relative to the server time.
127 | - The relative path in the `-ffmpeg` parameter is now determined relative to the location of the batch file (command run) directory, rather than relative to the location of *yrewind.exe*.
128 |
129 | ### Fixed
130 |
131 | - Several minor fixes.
132 |
133 | ### Removed
134 |
135 | - The ability to view a specified time interval using *VLC Media Player*.
136 |
137 | ## [21.051] - 2021-05-20
138 |
139 | ### Added
140 |
141 | - Support for arguments and nested quotes when using the `-executeonexit` parameter.
142 | - The ability to autorestart to get the next part of the stream (command `-executeonexit=*getnext*`).
143 |
144 | ### Fixed
145 |
146 | - Added an additional way to determine UTC time if the page of the required stream contains incorrect information (the reason for the hang of some streams).
147 | - Several minor fixes.
148 | - Updated algorithm for obtaining information about the stream.
149 |
150 | ## [21.041] - 2021-04-06
151 |
152 | ### Changed
153 |
154 | - The format of the metadata in the saved file (it is now formatted as: *title || author || live stream URL || channel URL || UTC start point*).
155 |
156 | ### Fixed
157 |
158 | - Error while saving file if its path contains invalid filesystem characters (when using rename masks `*author*` and `*title*`).
159 | - Several minor fixes.
160 |
161 | ## [21.031] - 2021-03-18
162 |
163 | ### Added
164 |
165 | - Support for saving audio files.
166 | - The `-executeonexit` parameter to run document or executable file after the program finishes.
167 |
168 | ### Fixed
169 |
170 | - Clarified the saved formats.
171 | - Several minor fixes.
172 |
173 | ## [21.023] - 2021-02-28
174 |
175 | ### Fixed
176 |
177 | - Several minor fixes.
178 |
179 | ## [21.022] - 2021-02-24
180 |
181 | ### Added
182 |
183 | - Single-character aliases for all parameters (`-u` for `-url`, `-s` for `-start`, etc.).
184 |
185 | ### Changed
186 |
187 | - The `-pathffmpeg` parameter has been renamed to `-ffmpeg`. The renamed parameter now supports relative paths, now you can also specify the path to *VLC Media Player* (to view the required time interval instead of saving it).
188 | - The `-pathsave` parameter has been renamed to `-output`. The renamed parameter now supports relative paths and rename masks for the output directory (the directory name must now end with a slash). In addition, the functionality of this parameter has been extended with the functionality of the `-filename` and `-vformat` deleted parameters.
189 |
190 | ### Fixed
191 |
192 | - Several minor fixes.
193 |
194 | ### Removed
195 |
196 | - The `-filename` and `-vformat` parameters (their functionality has been moved to the `-output` parameter).
197 |
198 | ## [21.021] - 2021-02-15
199 |
200 | ### Fixed
201 |
202 | - Several minor fixes.
203 |
204 | ## [21.015] - 2021-01-27
205 |
206 | ### Changed
207 |
208 | - Increased the maximum allowed video duration (up to 300 minutes).
209 |
210 | ## [21.014] - 2021-01-17
211 |
212 | ### Fixed
213 |
214 | - Several minor fixes.
215 |
216 | ## [21.013] - 2021-01-12
217 |
218 | ### Added
219 |
220 | - Parameter `-filename` to save live stream with custom file name (rename masks supported).
221 | - Parameter `-start=yyyyMMdd:hhmmss` to specify starting point with seconds.
222 | - Parameter `-url=[channelUrl]` to monitor the specified channel for new live streams.
223 |
224 | ### Fixed
225 |
226 | - The time in the file name now more closely matches the time of the streamer.
227 | - Several minor fixes.
228 |
229 | ## [21.012] - 2021-01-08
230 |
231 | ### Added
232 |
233 | - Option `-start=+[minutes]` for the delayed start of recording.
234 | - Streamer name to video file metadata.
235 | - Time in filename is now with seconds.
236 |
237 | ### Fixed
238 |
239 | - The bug due to which the program could not find information about the stream.
240 | - Several minor fixes.
241 |
242 | ## [21.011] - 2021-01-04
243 |
244 | ### Added
245 |
246 | - Option `-start=beginning` to rewind the live stream to the first available moment.
247 | - Option `-start=wait` to wait for the scheduled live stream to start and then automatically record it from the first second.
248 |
249 | ### Changed
250 |
251 | - The built-in help of the program has become a little more convenient.
252 |
253 | ### Fixed
254 |
255 | - The bug with FFmpeg freezing if stream terminated during real time recording.
256 | - Several minor fixes.
257 |
258 | ### Removed
259 |
260 | - The `-pathchrome` and `-nocache` parameters have been removed.
261 |
262 | ## [20.124] - 2020-12-31
263 |
264 | ### Changed
265 |
266 | - The speed of receiving information about live stream has been increased.
267 |
268 | ### Fixed
269 |
270 | - Several minor fixes.
271 |
272 | ### Removed
273 |
274 | - Dependency on Google Chrome. Now the browser is not required for the program to work.
275 |
276 | ## [20.123] - 2020-12-28
277 |
278 | ### Added
279 |
280 | - Parameter `-vformat=[formatExtension]`.
281 | - Support for resolutions higher than 1080p.
282 |
283 | ### Fixed
284 |
285 | - Several minor fixes.
286 |
287 | ## [20.122] - 2020-12-25
288 |
289 | ### Changed
290 |
291 | - Improved and accelerated work with cache.
292 | - Modes *rewind* and *real time* are combined: now it's possible to save intervals like `-start=-30 -duration=60` (the first part of the file is downloaded at high speed and the rest is recorded in real time).
293 |
294 | ### Fixed
295 |
296 | - The bug due to which all incomplete videos were without sound (for example, when the program was manually closed during recording).
297 | - Several minor fixes.
298 |
299 | ### Removed
300 |
301 | - Sync warning in file name if duration does not match specified. Now program just leaves temp file name.
302 |
303 | ## [20.121] - 2020-12-10
304 |
305 | ### Fixed
306 |
307 | - An issue where some streams could not be downloaded due to an error 9411 (*Cannot process live stream with FFmpeg library*).
308 | - Several minor fixes.
309 |
310 | ## [20.113] - 2020-11-28
311 |
312 | ### Added
313 |
314 | - The *real time* mode: now program can record live stream in real time.
315 |
316 | ### Changed
317 |
318 | - If the `-start` parameter is missing, the program now runs in real time recording mode, saving the *following* 1 hour of the stream, not the previous ones.
319 | - Improved speed of caching information about the required live stream.
320 | - Increased the maximum allowed video duration (up to 90 minutes).
321 |
322 | ### Fixed
323 |
324 | - The bug that caused an exception to be thrown when specifying a non-absolute path to the `-pathsave` parameter.
325 | - Several minor fixes.
326 |
327 | ## [20.112] - 2020-11-16
328 |
329 | ### Added
330 |
331 | - Preliminary internet connection check to prevent FFmpeg freezing.
332 |
333 | ### Changed
334 |
335 | - Increased the maximum allowed video duration (up to 75 minutes).
336 |
337 | ### Fixed
338 |
339 | - The bug with incorrect URL recognition if it was specified without quotes and contained a hyphen.
340 | - Several minor fixes.
341 |
342 | ## [20.111] - 2020-11-03
343 |
344 | ### Added
345 |
346 | - Parameter `-start=-[minutes]`.
347 |
348 | ### Fixed
349 |
350 | - Several minor fixes.
351 |
352 | ## [20.105] - 2020-10-31
353 |
354 | ### Added
355 |
356 | - Duration checking for downloaded videos.
357 |
358 | ### Fixed
359 |
360 | - An error 9124 (*FFmpeg not found*) if *bat* file was located in a different directory than program.
361 | - Several minor fixes.
362 |
363 | ## [20.104] - 2020-10-13
364 |
365 | ### Added
366 |
367 | - Saving metadata to the output video.
368 |
369 | ## [20.103] - 2020-10-12
370 |
371 | ### Changed
372 |
373 | - Command line arguments are now case insensitive.
374 |
375 | ### Fixed
376 |
377 | - Several minor fixes.
378 |
379 | ## [20.102] - 2020-10-07
380 |
381 | ### Fixed
382 |
383 | - Several minor fixes.
384 |
385 | ## [20.101] - 2020-10-06
386 |
387 | ### Added
388 |
389 | - Checking if other instances of the program is running.
390 | - Determining of the earliest available live stream time point.
391 | - The ability to cache live stream information to improve save speed.
392 |
393 | ### Changed
394 |
395 | - Now the program determines the nearest lower resolution if nonexistent is specified (instead of higher).
396 | - The program interface has been redesigned.
397 |
398 | ### Fixed
399 |
400 | - The bug when empty directories created by the current instance of the program (for example, if the video did not downloaded) were not deleted.
401 | - The bug causing the duration of some videos to be several seconds longer than the specified one.
402 | - Several minor fixes.
403 |
404 | ## [20.075] - 2020-07-30
405 |
406 | ### Added
407 |
408 | - Checking for interval availability before downloading it.
409 |
410 | ### Fixed
411 |
412 | - The bug with playing live stream sound when receiving information about it.
413 | - Several minor fixes.
414 |
415 | ## [20.074] - 2020-07-21
416 |
417 | ### Changed
418 |
419 | - To reduce the file size, the assembly of the program has been moved from .NET Core to .NET Framework.
420 |
421 | ### Fixed
422 |
423 | - Several minor fixes.
424 |
425 | ## [20.073] - 2020-07-20
426 |
427 | ### Added
428 |
429 | - Recognition of different URL spellings.
430 |
431 | ### Fixed
432 |
433 | - Several minor fixes.
434 |
435 | ## [20.072] - 2020-07-19
436 |
437 | ### Added
438 |
439 | - The function showing download progress.
440 | - Built-in help (with contents of *readme* file).
441 | - Live stream title parsing.
442 | - Video type recognition (live stream or regular video).
443 | - Video is now first saved under a temporary file name to prevent overwriting in case of an error.
444 |
445 | ### Fixed
446 |
447 | - Several minor fixes.
448 |
449 | ### Removed
450 |
451 | - The ability to download video files without limiting the duration.
452 |
453 | ## [20.071] - 2020-07-06
454 |
455 | ### Added
456 |
457 | - Basic functionality developed.
458 |
459 | [24.061]: https://github.com/rytsikau/ee.Yrewind/releases/download/20240622/ee.yrewind_24.061.zip
460 | [23.081]: https://github.com/rytsikau/ee.Yrewind/releases/download/20230804/ee.yrewind_23.081.zip
461 | [23.071]: https://github.com/rytsikau/ee.Yrewind/releases/download/20230725/ee.yrewind_23.071.zip
462 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ee.Yrewind
2 |
3 | Yrewind is a command line utility for saving YouTube live streams in original quality.
4 |
5 | The program has the following features:
6 |
7 | * Delayed start recording
8 | * Recording in real time
9 | * Downloading recently finished live streams
10 | * Rewinding to the specified time point in the past, and downloading from that point
11 | * Waiting for the scheduled live stream to start and then automatically recording from the first second
12 | * Monitoring the specified channel for new live streams and then automatically recording from the first second
13 |
14 | Supported parameters:
15 |
16 | [ ` -url (-u) ` ](#-urlurl)
17 | [ ` -start (-s) ` ](#-startyyyymmddhhmm--startyyyymmddhhmmss)
18 | [ ` -duration (-d) ` ](#-durationminutes)
19 | [ ` -resolution (-r) ` ](#-resolutionheightpixels)
20 | [ ` -ffmpeg (-f) ` ](#-ffmpegcpathtoffmpeg)
21 | [ ` -output (-o) ` ](#-outputcdir1dir2filenameextension)
22 | [ ` -browser (-b) ` ](#-browsercpathtobrowserfileexe)
23 | [ ` -cookie (-c) ` ](#-cookiecpathtocookiefileext)
24 | [ ` -keepstreaminfo (-k) ` ](#-keepstreaminfofalse)
25 | [ ` -log (-l) ` ](#-logtrue)
26 | [ ` -executeonexit (-e) ` ](#-executeonexitcpathtosomefileext)
27 |
28 |
29 |
30 | # [>> download <<](https://github.com/rytsikau/ee.Yrewind/releases/download/20240622/ee.yrewind_24.061.zip)
31 | Changelog is [here](https://github.com/rytsikau/ee.Yrewind/blob/main/CHANGELOG.md). If something doesn't work, please [report](https://github.com/rytsikau/ee.Yrewind/issues) and try [previous versions](https://github.com/rytsikau/ee.Yrewind/releases).
32 |
33 |
34 |
35 | ## Screenshot
36 |
37 |
38 |
39 |
40 |
41 | ## Quick start
42 |
43 | 1. Unpack the downloaded zip
44 | 2. Open *run.bat* in a text editor and paste the URLs of required streams instead of existing samples
45 | 3. Save *run.bat* and run it
46 | * Some [examples](#examples)
47 | * Since 2022, YouTube throttle third party downloaders. So, for faster operation, use the `-b` parameter pointing to the LEGACY browser. For example, Chrome with version 110 and [below](https://portableapps.com/downloading/?a=GoogleChromePortableLegacyWin7&s=s&p=&d=pa&f=GoogleChromePortableLegacyWin7_109.0.5414.120_online.paf.exe) works well
48 |
49 |
50 |
51 | ## Usage
52 |
53 | The only required command line argument is the `-url`:
54 |
55 | ##### [**` -url=[url] `**](#)
56 |
57 | With this command, the program records a livestream in real time for 1 hour at 1080p resolution, or at a lower if 1080p is not available. URL can be specified in various formats:
58 | > yrewind -url='youtube.com/watch?v=oJUvTVdTMyY'
59 | > yrewind -url=https://www.youtu.be/oJUvTVdTMyY
60 | > yrewind -url=oJUvTVdTMyY
61 | > (etc.)
62 |
63 | Channel URL can also be specified, this allows to wait for a new livestream on the channel and automatically start recording from the first second when it starts. Please note that when specifying a channel URL, active livestreams are ignored, the program will wait for a new one.
64 | > yrewind -url='https://www.youtube.com/c/SkyNews'
65 | > yrewind -url=www.youtube.com/user/SkyNews/
66 | > yrewind -url='youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ'
67 | > yrewind -url=UCoMdktPbSTixAyNGwb-UYkQ
68 |
69 |
70 |
71 | To rewind the livestream or delay the start of recording, use the `-start` parameter. It has several spellings:
72 |
73 | ##### [**` -start=[YYYYMMDD:hhmm], -start=[YYYYMMDD:hhmmss] `**](#)
74 | ##### [**` -start=[Y:hhmm], -start=[Y:hhmmss] `**](#)
75 | ##### [**` -start=[T:hhmm], -start=[T:hhmmss] `**](#)
76 | ##### [**` -start=beginning, -start=b `**](#)
77 | ##### [**` -start=-[minutes] `**](#)
78 | ##### [**` -start=+[minutes] `**](#)
79 |
80 | This parameter specifies the point in time from which to save the stream. It's calculated relative to the moment the program was started (displayed in the first line). If the parameter is missing, program saves ongoing livestream in real time, scheduled and finished - from the beginning. Depending on technical parameters of the livestream, start point may be shifted from the requested one by several seconds (in a larger direction).
81 |
82 | To download the time interval from 7:10AM to 8:10AM on July 15, 2020:
83 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=20200715:0710
84 |
85 | To download the time interval from yesterday 10:15PM to 11:15PM:
86 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=Y:2215
87 |
88 | To download the time interval from today 02:00AM to 03:00AM:
89 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=T:020000
90 |
91 | To download from the first currently available moment:
92 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=beginning
93 |
94 | To download the time interval from 3 hours ago to 2 hours ago:
95 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=-180
96 |
97 | To wait 2 hours, then record for 1 hour:
98 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=+120
99 |
100 |
101 |
102 | The program also has several other parameters:
103 |
104 | ##### [**` -duration=[minutes] `**](#)
105 | ##### [**` -duration=[minutes].[seconds] `**](#)
106 |
107 | Specifies the required duration. The minimum value is 0.01 (1 second), the maximum is limited to 300 (5 hours). If the parameter is missing, program uses the default 1 hour. Depending on technical parameters of the livestream, result duration may differ from the requested one by several seconds (in a larger direction). The examples below saves 300 minutes of the livestream:
108 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=300
109 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=300.00
110 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=max
111 |
112 |
113 |
114 | ##### [**` -resolution=[heightPixels] `**](#)
115 |
116 | Specifies the required resolution in pixels (height). If this parameter is missing, program uses the default 1080. If the requested resolution is not available, the nearest lower will be selected. In the examples below, the livestream will be saved at 144p:
117 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=144
118 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=200
119 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=1
120 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=min
121 |
122 |
123 |
124 | ##### [**` -ffmpeg='c:\path\to\ffmpeg\' `**](#)
125 |
126 | Specifies the path to FFmpeg library. If relative path is specified, the base folder is the folder from which the command line was run. If this parameter is missing, Yrewind tries to find FFmpeg in its own folder and using environment variables.
127 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -ffmpeg='c:\Program Files\FFmpeg\'
128 |
129 |
130 |
131 | ##### [**` -output='c:\dir1\dir2\filename.extension' `**](#)
132 |
133 | Specifies custom folder, filename and extension (media container format) for the saved livestream. If relative path is specified, the base folder is the folder from which the command line was run. If this parameter is missing, program uses the next default values:
134 | * `[batch file folder]\saved_streams\` - for folder
135 | * `[id]_[date]-[time]_[duration]_[resolution]` - for filename
136 | * `.mkv` - for extension
137 |
138 | The `-output` parameter can be specified partially, then the missing parts are replaced with default values. In this case, the part of the string to the right of the last slash is interpreted as filename and/or extension. If the string does not contain slashes, it's fully interpreted as filename and/or extension:
139 | * `c:\dir1\dir2\` - custom folder, default filename, default extension
140 | * `dir1\filename` - custom subfolder, custom filename, default extension
141 | * `dir1\.extension` - custom subfolder, default filename, custom extension
142 | * `filename` - default folder, custom filename, default extension
143 | * `.extension` - default folder, default filename, custom extension
144 |
145 | Folder and filename supports renaming masks: `*id*`, `*start*`, `*start[customDateTime]*` (recognizes letters yyyyMMddHHmmss), `*duration*`, `*resolution*`, `*channel_id*`, `*author*` and `*title*`.
146 |
147 | The extension defines the format of the media container in which the livestream will be saved. Formats description:
148 | * `.avi`, `.mp4` - use AVC and MP4a data; if AVC is unavailable, use VP9
149 | * `.asf`, `.mkv`, `.wmv` - use VP9 and MP4a data; if VP9 is unavailable, use AVC
150 | * `.3gp`, ` .flv`, ` .mov`, `.ts` - use AVC and MP4a data; doesn't support high resolutions - saves at 1080p even if requested higher resolution is available
151 | * `.aac`, `.m4a`, `.wma` - use MP4a data; saves only audio (to save only audio, you can also specify zero resolution `-r=0` - this works with all audio and video formats except `.mp4`)
152 | * `.m3u`, `.m3u8` - playlist files pointing to livestream on the Internet, allows watching in media player without downloading. Tested with VLC. Shelf life of playlist files is only 6 hours
153 |
154 | The example below saves the livestream as *\saved_streams\oJUvTVdTMyY_20210123-185830_060.00m_1080p.ts*:
155 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -output=.ts
156 |
157 | The next example saves the livestream as *d:\Streams\Sky News\2021-01-12_12-34.mkv*:
158 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -output='d:\Streams\*author*\*start[yyyy-MM-dd_HH-mm]*'
159 |
160 |
161 |
162 | ##### [**` -browser='c:\path\to\browser\file.exe' `**](#)
163 |
164 | Allows to use an alternative browser to get technical information about the livestream. For the portable version of browser, specify the full path to the executable file; for the installed version, it's usually enough to specify the name. Only Chromium-based browsers are supported - Chrome, Edge, Brave, Opera, Vivaldi, etc. If this parameter is missing, program uses pre-installed MS Edge. If this parameter is set to `false`, Yrewind will not use browser, but this may slow down the download speed.
165 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -browser='d:\Portable programs\Vivaldi\Application\vivaldi.exe'
166 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -browser=chrome
167 |
168 |
169 |
170 | ##### [**` -cookie='c:\path\to\cookie\file.ext' `**](#)
171 |
172 | Specifies the path to the cookie file. If relative path is specified, the base folder is the folder from which the command line was run. The parameter can be useful if YouTube requires a captcha or authorization to access a livestream with age or membership restrictions. The cookie file must be in Netscape format and can be obtained using any suitable browser add-on. Please note that cookie created after solving captcha is usable for only a few hours. Instead of solving captcha, it's better to log in to YouTube and create cookie after that.
173 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -cookie='cookies.txt'
174 |
175 |
176 |
177 | ##### [**` -keepstreaminfo=false `**](#)
178 |
179 | If this parameter is set to `false`, Yrewind will not keep technical information about the livestream in a temporary cache file, and will delete this file if it exists.
180 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -keepstreaminfo=false
181 |
182 |
183 |
184 | ##### [**` -log=true `**](#)
185 |
186 | If this parameter is set to `true`, Yrewind will generate log files (in the folder from which the command line was run).
187 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -log=true
188 |
189 |
190 |
191 | ##### [**` -executeonexit='c:\path\to\some\file.ext' `**](#)
192 |
193 | Specifies the command to run after Yrewind exits. If it's an executable file, you can also specify the arguments it supports (don't forget the quotes - nested supported). The non-executable file will be launched by the associated program. The parameter supports two rename masks - `*output*`, which contains the full path of the saved video, and `*getnext*`, which contains the command to start Yrewind again to get the next interval of the stream. When using `-executeonexit=*getnext*` command inside a batch file, keep in mind that this file is first executed to the end, and only then the `*getnext*` command is executed. Also use rename masks `*start*` and `*start[customDateTime]*` to avoid duplicate names of saved stream parts (or just use default auto-naming). In the first example below, the saved video will be opened by the associated program, in the second - using the specified program:
194 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -executeonexit=*output*
195 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -executeonexit=''c:\Program Files\VLC mediaplayer\vlc.exe' *output*'
196 |
197 |
198 |
199 | ## Examples
200 |
201 | Save 1 hour of the stream from 04:55AM to 05:55AM on May 5, 2020, at 720p, to specified folder:
202 | > yrewind -u=oJUvTVdTMyY -s=20200505:0455 -r=720 -o='d:\Streams\'
203 |
204 | Save 89 minutes 30 seconds of the stream from today 10:45AM to 12:15PM, at 1080p:
205 | > yrewind -u=oJUvTVdTMyY -s=T:1045 -d=89.30
206 |
207 | Record livestream until it ends, starting from the beginning, in `.ts` format, save result video to desktop:
208 | > yrewind -u=oJUvTVdTMyY -s=b -o=%UserProfile%\Desktop\.ts -e=*getnext*
209 |
210 | Immediately play (without downloading) with assotiated mediaplayer, from yesterday 03:00AM, at the maximum available resolution:
211 | > yrewind -u=oJUvTVdTMyY -s=Y:0300 -r=max -o=.m3u -e=*output*
212 |
213 | Wait for a new livestream on the specified channel, then start recording from the 30th minute:
214 | > yrewind -u=https://www.youtube.com/c/SkyNews -s=+30
215 |
216 | Batch file example (runs 3 copies of the program at the same time):
217 | ```
218 | @echo off
219 | set O='d:\Streams\'
220 | set B='c:\Program Files\Chrome\chrome.exe'
221 | set F='c:\Program Files\FFmpeg\'
222 | set Y='c:\Program Files\ee.Yrewind\'
223 | cd /d %Y%
224 |
225 | set U=oJUvTVdTMyY
226 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1500
227 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1600
228 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1700
229 | ```
230 |
231 |
232 |
233 | ## Notes
234 |
235 | * Loss of packets on the streamer side causes the estimated time to shift. The offset is usually seconds, but if its internet connection is unstable and/or the stream has been running for a long time, it can be minutes or even hours. For example, if the stream was interrupted for a total of 1 hour, then 24-hour frames will be presented as 23-hour. Thus, start point time accuracy can only be guaranteed for the current moment. The further the livestream is rewound, the less accuracy. Also, if there are interruptions in the livestream at the specified time interval, the duration of the saved file will be shorter by the total duration of those interruptions; a warning for this incompleted file will be displayed
236 | * Occasionally, the message `unable to verify the saved file is correct` appears. The reasons may be as follows: if the duration of the saved file cannot be verified (there is a possibility that the file is damaged); if the duration of the saved file does not match the requested one (also in this case, the output file name contains the word *INCOMPLETE*); if the starting point of the requested time interval cannot be accurately determined (for example due to server side error)
237 | * To reduce the chance of output file corruption, it's better not to use Moov Atom formats (`.3gp`, `.mov`, `.mp4`, `.m4a`) for long recordings. Also, these formats don't allow to play a file that is in the process of downloading (other formats do)
238 | * Recently finished livestream can be downloaded within approximately 6 hours of its completion. After this time, such stream 'turns' into a regular video and can be downloaded, for example, using Youtube-dl
239 | * When using proxy, VPN or special firewall settings, keep in mind that not only Yrewind should have appropriate access, but also FFmpeg
240 |
241 |
242 |
243 | ## Terms of use
244 |
245 | * This software provides access to information on the Internet that is publicly available and does not require authorization or authentication
246 | * This software is free for non-commercial use and is provided 'as is' without warranty of any kind, either express or implied
247 | * The author will not be liable for data loss, damages or any other kind of loss while using or misusing this software
248 | * The author will not be liable for the misuse of content obtained using this software, including copyrighted, age-restricted, or any other protected content
249 |
250 |
251 |
252 | ## Developer info
253 |
254 | * C#
255 | * .NET Framework 4.8
256 | * Visual Studio Community 2022
257 |
258 |
259 |
260 | ## Requirements
261 |
262 | * FFmpeg static build (included in the archive)
263 | * Windows 7 and on / Windows Server 2008 R2 and on
264 | * Chromium-based browser
265 |
266 |
267 |
268 | ## Tested configuration
269 |
270 | * FFmpeg 4.3 x86 (by Zeranoe)
271 | * Windows 10 Pro x32
272 | * Windows 10 Pro x64
273 |
274 |
275 |
276 | ## About
277 |
278 | Console utility for saving YouTube live streams with rewind function up to 167 hours
279 |
280 |
281 |
282 | ## Tags
283 |
284 | download downloader dvr live livestream record rewind save stream youtube
285 |
286 | ---
287 | [[program page]](https://rytsikau.github.io/ee.Yrewind) [[start page]](https://rytsikau.github.io) [[author e-mail]](mailto:y.rytsikau@gmail.com)
288 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-leap-day
--------------------------------------------------------------------------------
/_updated.log:
--------------------------------------------------------------------------------
1 | 240622;https://github.com/rytsikau/ee.Yrewind/releases/download/20240622/ee.yrewind_24.061.zip
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rytsikau/ee.Yrewind/d9e4f994fb345cd5c28005f8a89d316ebf92b6bb/screenshot.png
--------------------------------------------------------------------------------
/source/Cache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace yrewind
8 | {
9 | // Caching the technical information about the stream in a temporary file
10 | // Line format for each live stream:
11 | // [dateTimeOfGettingInfo] [id] [idStatus] [channelId] [uriAdirect] [uriVdirect] [jsonHtmlStr]
12 | class Cache
13 | {
14 | #region Read - Read required data from cache
15 | public void Read(
16 | string id,
17 | out string idStatus,
18 | out string channelId,
19 | out string uriAdirect,
20 | out string uriVdirect,
21 | out string jsonHtmlStr
22 | )
23 | {
24 | idStatus = string.Empty;
25 | channelId = string.Empty;
26 | uriAdirect = string.Empty;
27 | uriVdirect = string.Empty;
28 | jsonHtmlStr = string.Empty;
29 |
30 | foreach (var line in GetContent())
31 | {
32 | if (line == string.Empty) continue;
33 |
34 | var lineId = line.Split('\t')[1];
35 |
36 | if (id.Trim() == lineId.Trim())
37 | {
38 | idStatus = line.Split('\t')[2];
39 | channelId = line.Split('\t')[3];
40 | uriAdirect = line.Split('\t')[4];
41 | uriVdirect = line.Split('\t')[5];
42 | jsonHtmlStr = line.Split('\t')[6];
43 | }
44 | }
45 | }
46 | #endregion
47 |
48 | #region Write - Write cache file
49 | public void Write(
50 | string id,
51 | string idStatus,
52 | string channelId,
53 | string uriAdirect,
54 | string uriVdirect,
55 | string jsonHtmlStr
56 | )
57 | {
58 | var content = GetContent();
59 | var newData =
60 | (Program.Start + Program.Timer.Elapsed).ToString("yyyyMMdd-HHmmss") + '\t' +
61 | id + '\t' +
62 | idStatus + '\t' +
63 | channelId + '\t' +
64 | uriAdirect + '\t' +
65 | uriVdirect + '\t' +
66 | jsonHtmlStr;
67 |
68 | var match = content.FirstOrDefault(x => x.Contains("\t" + id + "\t"));
69 | if (match == null)
70 | {
71 | content.Add(newData);
72 | }
73 | else if ((match.Split('\t')[4] == string.Empty) & (uriAdirect != string.Empty))
74 | {
75 | // Prefer data containing direct browser URLs
76 | content.Remove(match);
77 | content.Add(newData);
78 | }
79 |
80 | if (Validator.Log)
81 | {
82 | Program.Log(string.Join(Environment.NewLine, content), "cache");
83 | }
84 |
85 | try
86 | {
87 | File.WriteAllLines(Constants.PathCache, content);
88 | }
89 | catch (Exception e)
90 | {
91 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
92 | if (Validator.Log) Program.Log(Program.ErrInfo);
93 | }
94 | }
95 | #endregion
96 |
97 | #region Delete - Delete cache file
98 | public void Delete()
99 | {
100 | try
101 | {
102 | File.Delete(Constants.PathCache);
103 | }
104 | catch (Exception e)
105 | {
106 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
107 | if (Validator.Log) Program.Log(Program.ErrInfo);
108 | }
109 | }
110 | #endregion
111 |
112 | #region GetContent - Read unexpired data from cache file
113 | List GetContent()
114 | {
115 | DateTime dtAdded;
116 | var content = new List();
117 |
118 | try
119 | {
120 | if (new FileInfo(Constants.PathCache).Length > 1000000) throw new Exception();
121 |
122 | foreach (var line in File.ReadAllLines(Constants.PathCache))
123 | {
124 | if (line == string.Empty) continue;
125 |
126 | dtAdded = DateTime.ParseExact(line.Split('\t')[0], "yyyyMMdd-HHmmss", null);
127 | var dtExpiration = dtAdded.AddMinutes(Constants.CacheShelflifeMinutes);
128 | if (Program.Start + Program.Timer.Elapsed > dtExpiration) continue;
129 |
130 | content.Add(line);
131 | }
132 | }
133 | catch (Exception e)
134 | {
135 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
136 | if (Validator.Log) Program.Log(Program.ErrInfo);
137 | }
138 |
139 | return content;
140 | }
141 | #endregion
142 | }
143 | }
--------------------------------------------------------------------------------
/source/Constants.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Reflection;
5 |
6 | namespace yrewind
7 | {
8 | // Storing messages, readme text and various constants
9 | static class Constants
10 | {
11 | #region Msg - Messages
12 | public static readonly Dictionary Msg = new Dictionary
13 | {
14 | [9000] = "[class Program]",
15 | [9050] = "----------------------------------------------------------------",
16 | [9051] = "{0} (version {1})",
17 | [9052] = "--> {0} (started at {1})",
18 | [9053] = "--> OK ({0})",
19 | [9054] = "--> ERROR {0}: {1} ({2})",
20 | [9055] = "",
21 | [9056] = "",
22 | [9057] = "Check '-output' argument",
23 | [9058] = "Checking",
24 | [9059] = "Command line input is too long",
25 | [9060] = "Current version is actual",
26 | [9061] = "Delayed start is limited to 24 hours",
27 | [9062] = "Downloading, please wait a minute",
28 | [9063] = "Error",
29 | [9064] = "File already exists",
30 | [9065] = "Getting live stream info",
31 | [9066] = "New version available",
32 | [9067] = "Output file",
33 | [9068] = "Please see the actual text on the program page",
34 | [9069] = "Press to download",
35 | [9070] = " exit",
36 | [9071] = " show help",
37 | [9072] = " check for updates",
38 | [9073] = "Ready",
39 | [9074] = "Waiting",
40 | [9075] = "YouTube not responding",
41 | [9076] = "Resolutions",
42 | [9077] = "Saving",
43 | [9078] = "See file '{0}' on the desktop",
44 | [9079] = "Skipped! File already exists",
45 | [9080] = "Stream title",
46 | [9081] = "Unable to check for updates",
47 | [9082] = "Unable to verify the saved file is correct",
48 | [9083] = "Use , , , to scroll",
49 |
50 | [9100] = "[class Validator]",
51 | [9110] = "Check for duplicate arguments on command line input",
52 | [9111] = "Cannot read cookie file",
53 | [9112] = "Check command line input",
54 | [9113] = "Required argument '-url' not found",
55 | [9114] = "Check '-url' argument",
56 | [9115] = "Check '-start' argument",
57 | [9116] = "Check '-duration' argument",
58 | [9117] = "Check '-resolution' argument",
59 | [9118] = "Check '-ffmpeg' argument",
60 | [9119] = "Check '-output' argument",
61 | [9120] = "Check '-browser' argument",
62 | [9121] = "Check '-cookie' argument",
63 |
64 | [9200] = "[class Waiter]",
65 | [9210] = "Cannot get live stream information",
66 | [9211] = "This live stream ended too long ago, now it's a regular video",
67 | [9212] = "Cannot get channel information",
68 | [9213] = "Cannot get channel information. If URL contains '%', is it escaped?",
69 | [9214] = "Video unavailable",
70 | [9215] = "It's not a live stream",
71 | [9216] = "Saving copyrighted live streams is blocked",
72 | [9217] = "Seems to be a restricted live stream, try '-b' or '-c' option",
73 | [9218] = "For an upcoming stream, start point cannot be in the past",
74 | [9219] = "Cannot get live stream direct URL",
75 | [9220] = "Cannot get live stream information with browser",
76 |
77 | [9300] = "[class Preparer]",
78 | [9310] = "Cannot get live stream information",
79 | [9311] = "Cannot get live stream information with browser",
80 | [9312] = "Further parts of the stream are unavailable",
81 | [9313] = "Requested time interval isn't available",
82 |
83 | [9400] = "[class Saver]",
84 | [9410] = "FFmpeg not responding",
85 | [9411] = "Output file(s) creating error",
86 | [9412] = "Output folder creating error",
87 | [9413] = "Output file isn't completed, there will be a retry",
88 |
89 | [9500] = "[class Cache]",
90 |
91 | [9900] = "",
92 | [9999] = "Unknown error"
93 | };
94 | #endregion
95 |
96 | #region Itag - Description of formats ('itag' codes)
97 | // [itag] = "container;content;resolution/bitrate;other(3d,hdr,vr,etc.)"
98 | public static readonly Dictionary Itag = new Dictionary
99 | {
100 | [5] = "flv;audio+video;240p30;-",
101 | [6] = "flv;audio+video;270p30;-",
102 | [17] = "3gp;audio+video;144p30;-",
103 | [18] = "mp4;audio+video;360p30;-",
104 | [22] = "mp4;audio+video;720p30;-",
105 | [34] = "flv;audio+video;360p30;-",
106 | [35] = "flv;audio+video;480p30;-",
107 | [36] = "3gp;audio+video;180p30;-",
108 | [37] = "mp4;audio+video;1080p30;-",
109 | [38] = "mp4;audio+video;3072p30;-",
110 | [43] = "webm;audio+video;360p30;-",
111 | [44] = "webm;audio+video;480p30;-",
112 | [45] = "webm;audio+video;720p30;-",
113 | [46] = "webm;audio+video;1080p30;-",
114 | [82] = "mp4;audio+video;360p30;3d",
115 | [83] = "mp4;audio+video;480p30;3d",
116 | [84] = "mp4;audio+video;720p30;3d",
117 | [85] = "mp4;audio+video;1080p30;3d",
118 | [92] = "hls;audio+video;240p30;3d",
119 | [93] = "hls;audio+video;360p30;3d",
120 | [94] = "hls;audio+video;480p30;3d",
121 | [95] = "hls;audio+video;720p30;3d",
122 | [96] = "hls;audio+video;1080p30;-",
123 | [100] = "webm;audio+video;360p30;3d",
124 | [101] = "webm;audio+video;480p30;3d",
125 | [102] = "webm;audio+video;720p30;3d",
126 | [132] = "hls;audio+video;240p30;-",
127 | [133] = "mp4;video;240p30;-",
128 | [134] = "mp4;video;360p30;-",
129 | [135] = "mp4;video;480p30;-",
130 | [136] = "mp4;video;720p30;-",
131 | [137] = "mp4;video;1080p30;-",
132 | [138] = "mp4;video;2160p60;-",
133 | [139] = "m4a;audio;48k;-",
134 | [140] = "m4a;audio;128k;-",
135 | [141] = "m4a;audio;256k;-",
136 | [151] = "hls;audio+video;72p30;-",
137 | [160] = "mp4;video;144p30;-",
138 | [167] = "webm;video;360p30;-",
139 | [168] = "webm;video;480p30;-",
140 | [169] = "webm;video;1080p30;-",
141 | [171] = "webm;audio;128k;-",
142 | [218] = "webm;video;480p30;-",
143 | [219] = "webm;video;144p30;-",
144 | [242] = "webm;video;240p30;-",
145 | [243] = "webm;video;360p30;-",
146 | [244] = "webm;video;480p30;-",
147 | [245] = "webm;video;480p30;-",
148 | [246] = "webm;video;480p30;-",
149 | [247] = "webm;video;720p30;-",
150 | [248] = "webm;video;1080p30;-",
151 | [249] = "webm;audio;50k;-",
152 | [250] = "webm;audio;70k;-",
153 | [251] = "webm;audio;160k;-",
154 | [264] = "mp4;video;1440p30;-",
155 | [266] = "mp4;video;2160p60;-",
156 | [271] = "webm;video;1440p30;-",
157 | [272] = "webm;video;2880p30;-",
158 | [278] = "webm;video;144p30;-",
159 | [298] = "mp4;video;720p60;-",
160 | [299] = "mp4;video;1080p60;-",
161 | [302] = "webm;video;720p60;-",
162 | [303] = "webm;video;1080p60;-",
163 | [308] = "webm;video;1440p60;-",
164 | [313] = "webm;video;2160p30;-",
165 | [315] = "webm;video;2160p60;-",
166 | [330] = "webm;video;144p60;hdr",
167 | [331] = "webm;video;240p60;hdr",
168 | [332] = "webm;video;360p60;hdr",
169 | [333] = "webm;video;480p60;hdr",
170 | [334] = "webm;video;720p60;hdr",
171 | [335] = "webm;video;1080p60;hdr",
172 | [336] = "webm;video;1440p60;hdr",
173 | [337] = "webm;video;2160p60;hdr",
174 | [394] = "mp4;video;144p30;-",
175 | [395] = "mp4;video;240p30;-",
176 | [396] = "mp4;video;360p30;-",
177 | [397] = "mp4;video;480p30;-",
178 | [398] = "mp4;video;720p30;-",
179 | [399] = "mp4;video;1080p30;-",
180 | [400] = "mp4;video;1440p30;-",
181 | [401] = "mp4;video;2160p30;-",
182 | [402] = "mp4;video;2880p30;-",
183 | };
184 | #endregion
185 |
186 | #region Help - Program readme text
187 | public static readonly string Help = @"
188 | # ee.Yrewind
189 |
190 | Yrewind is a command line utility for saving YouTube live streams in original quality.
191 |
192 | The program has the following features:
193 |
194 | * Delayed start recording
195 | * Recording in real time
196 | * Downloading recently finished live streams
197 | * Rewinding to the specified time point in the past, and downloading from that point
198 | * Waiting for the scheduled live stream to start and then automatically recording from the first second
199 | * Monitoring the specified channel for new live streams and then automatically recording from the first second
200 |
201 |
202 |
203 | # Supported parameters
204 |
205 | -url (-u)
206 | -start (-s)
207 | -duration (-d)
208 | -resolution (-r)
209 | -ffmpeg (-f)
210 | -output (-o)
211 | -browser (-b)
212 | -cookie (-c)
213 | -keepstreaminfo (-k)
214 | -log (-l)
215 | -executeonexit (-e)
216 |
217 |
218 |
219 | # Links
220 |
221 | Download: https://github.com/rytsikau/ee.Yrewind/releases/download/20240622/ee.yrewind_24.061.zip
222 | Changelog: https://github.com/rytsikau/ee.Yrewind/blob/main/CHANGELOG.md
223 | Report an issue: https://github.com/rytsikau/ee.Yrewind/issues
224 | Previous versions: https://github.com/rytsikau/ee.Yrewind/releases
225 | Program screenshot: https://github.com/rytsikau/ee.yrewind/raw/main/screenshot.png
226 |
227 |
228 |
229 | # Quick start
230 |
231 | 1. Unpack the downloaded zip
232 | 2. Open *run.bat* in a text editor and paste the URLs of required streams instead of existing samples
233 | 3. Save *run.bat* and run it
234 | * Some [examples](#examples)
235 |
236 |
237 |
238 | # Usage
239 |
240 | The only required command line argument is the `-url`:
241 |
242 | -url=[url]
243 |
244 | With this command, the program records a livestream in real time for 1 hour at 1080p resolution, or at a lower if 1080p is not available. URL can be specified in various formats:
245 | > yrewind -url='youtube.com/watch?v=oJUvTVdTMyY'
246 | > yrewind -url=https://www.youtu.be/oJUvTVdTMyY
247 | > yrewind -url=oJUvTVdTMyY
248 | > (etc.)
249 |
250 | Channel URL can also be specified, this allows to wait for a new livestream on the channel and automatically start recording from the first second when it starts. Please note that when specifying a channel URL, active livestreams are ignored, the program will wait for a new one.
251 | > yrewind -url='https://www.youtube.com/c/SkyNews'
252 | > yrewind -url=www.youtube.com/user/SkyNews/
253 | > yrewind -url='youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ'
254 | > yrewind -url=UCoMdktPbSTixAyNGwb-UYkQ
255 |
256 |
257 |
258 | To rewind the livestream or delay the start of recording, use the `-start` parameter. It has several spellings:
259 |
260 | -start=[YYYYMMDD:hhmm], -start=[YYYYMMDD:hhmmss]
261 | -start=[Y:hhmm], -start=[Y:hhmmss]
262 | -start=[T:hhmm], -start=[T:hhmmss]
263 | -start=beginning, -start=b
264 | -start=-[minutes]
265 | -start=+[minutes]
266 |
267 | This parameter specifies the point in time from which to save the stream. It's calculated relative to the moment the program was started (displayed in the first line). If the parameter is missing, program saves ongoing livestream in real time, scheduled and finished - from the beginning. Depending on technical parameters of the livestream, start point may be shifted from the requested one by several seconds (in a larger direction).
268 |
269 | To download the time interval from 7:10AM to 8:10AM on July 15, 2020:
270 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=20200715:0710
271 |
272 | To download the time interval from yesterday 10:15PM to 11:15PM:
273 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=Y:2215
274 |
275 | To download the time interval from today 02:00AM to 03:00AM:
276 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=T:020000
277 |
278 | To download from the first currently available moment:
279 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=beginning
280 |
281 | To download the time interval from 3 hours ago to 2 hours ago:
282 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=-180
283 |
284 | To wait 2 hours, then record for 1 hour:
285 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -start=+120
286 |
287 |
288 |
289 | The program also has several other parameters:
290 |
291 | -duration=[minutes]
292 | -duration=[minutes].[seconds]
293 |
294 | Specifies the required duration. The minimum value is 0.01 (1 second), the maximum is limited to 300 (5 hours). If the parameter is missing, program uses the default 1 hour. Depending on technical parameters of the livestream, result duration may differ from the requested one by several seconds (in a larger direction). The examples below saves 300 minutes of the livestream:
295 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=300
296 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=300.00
297 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -duration=max
298 |
299 |
300 |
301 | -resolution=[heightPixels]
302 |
303 | Specifies the required resolution in pixels (height). If this parameter is missing, program uses the default 1080. If the requested resolution is not available, the nearest lower will be selected. In the examples below, the livestream will be saved at 144p:
304 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=144
305 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=200
306 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=1
307 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -resolution=min
308 |
309 |
310 |
311 | -ffmpeg='c:\path\to\ffmpeg\'
312 |
313 | Specifies the path to FFmpeg library. If relative path is specified, the base folder is the folder from which the command line was run. If this parameter is missing, Yrewind tries to find FFmpeg in its own folder and using environment variables.
314 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -ffmpeg='c:\Program Files\FFmpeg\'
315 |
316 |
317 |
318 | -output='c:\dir1\dir2\filename.extension'
319 |
320 | Specifies custom folder, filename and extension (media container format) for the saved livestream. If relative path is specified, the base folder is the folder from which the command line was run. If this parameter is missing, program uses the next default values:
321 | * `[batch file folder]\saved_streams\` - for folder
322 | * `[id]_[date]-[time]_[duration]_[resolution]` - for filename
323 | * `.mkv` - for extension
324 |
325 | The `-output` parameter can be specified partially, then the missing parts are replaced with default values. In this case, the part of the string to the right of the last slash is interpreted as filename and/or extension. If the string does not contain slashes, it's fully interpreted as filename and/or extension:
326 | * `c:\dir1\dir2\` - custom folder, default filename, default extension
327 | * `dir1\filename` - custom subfolder, custom filename, default extension
328 | * `dir1\.extension` - custom subfolder, default filename, custom extension
329 | * `filename` - default folder, custom filename, default extension
330 | * `.extension` - default folder, default filename, custom extension
331 |
332 | Folder and filename supports renaming masks: `*id*`, `*start*`, `*start[customDateTime]*` (recognizes letters yyyyMMddHHmmss), `*duration*`, `*resolution*`, `*channel_id*`, `*author*` and `*title*`.
333 |
334 | The extension defines the format of the media container in which the livestream will be saved. Formats description:
335 | * `.avi`, `.mp4` - use AVC and MP4a data; if AVC is unavailable, use VP9
336 | * `.asf`, `.mkv`, `.wmv` - use VP9 and MP4a data; if VP9 is unavailable, use AVC
337 | * `.3gp`, ` .flv`, ` .mov`, `.ts` - use AVC and MP4a data; doesn't support high resolutions - saves at 1080p even if requested higher resolution is available
338 | * `.aac`, `.m4a`, `.wma` - use MP4a data; saves only audio (to save only audio, you can also specify zero resolution `-r=0` - this works with all audio and video formats except `.mp4`)
339 | * `.m3u`, `.m3u8` - playlist files pointing to livestream on the Internet, allows watching in media player without downloading. Tested with VLC. Shelf life of playlist files is only 6 hours
340 |
341 | The example below saves the livestream as *\saved_streams\oJUvTVdTMyY_20210123-185830_060.00m_1080p.ts*:
342 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -output=.ts
343 |
344 | The next example saves the livestream as *d:\Streams\Sky News\2021-01-12_12-34.mkv*:
345 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -output='d:\Streams\*author*\*start[yyyy-MM-dd_HH-mm]*'
346 |
347 |
348 |
349 | -browser='c:\path\to\browser\file.exe'
350 |
351 | Allows to use an alternative browser to get technical information about the livestream. For the portable version of browser, specify the full path to the executable file; for the installed version, it's usually enough to specify the name. Only Chromium-based browsers are supported - Chrome, Edge, Brave, Opera, Vivaldi, etc. If this parameter is missing, program uses pre-installed MS Edge. If this parameter is set to `false`, Yrewind will not use browser, but this may slow down the download speed.
352 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -browser='d:\Portable programs\Vivaldi\Application\vivaldi.exe'
353 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -browser=chrome
354 |
355 |
356 |
357 | -cookie='c:\path\to\cookie\file.ext'
358 |
359 | Specifies the path to the cookie file. If relative path is specified, the base folder is the folder from which the command line was run. The parameter can be useful if YouTube requires a captcha or authorization to access a livestream with age or membership restrictions. The cookie file must be in Netscape format and can be obtained using any suitable browser add-on. Please note that cookie created after solving captcha is usable for only a few hours. Instead of solving captcha, it's better to log in to YouTube and create cookie after that.
360 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -cookie='cookies.txt'
361 |
362 |
363 |
364 | -keepstreaminfo=false
365 |
366 | If this parameter is set to `false`, Yrewind will not keep technical information about the livestream in a temporary cache file, and will delete this file if it exists.
367 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -keepstreaminfo=false
368 |
369 |
370 |
371 | -log=true
372 |
373 | If this parameter is set to `true`, Yrewind will generate log files (in the folder from which the command line was run).
374 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -log=true
375 |
376 |
377 |
378 | -executeonexit='c:\path\to\some\file.ext'
379 |
380 | Specifies the command to run after Yrewind exits. If it's an executable file, you can also specify the arguments it supports (don't forget the quotes - nested supported). The non-executable file will be launched by the associated program. The parameter supports two rename masks - `*output*`, which contains the full path of the saved video, and `*getnext*`, which contains the command to start Yrewind again to get the next interval of the stream. When using `-executeonexit=*getnext*` command inside a batch file, keep in mind that this file is first executed to the end, and only then the `*getnext*` command is executed. Also use rename masks `*start*` and `*start[customDateTime]*` to avoid duplicate names of saved stream parts (or just use default auto-naming). In the first example below, the saved video will be opened by the associated program, in the second - using the specified program:
381 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -executeonexit=*output*
382 | > yrewind -url='https://www.youtube.com/watch?v=oJUvTVdTMyY' -executeonexit=''c:\Program Files\VLC mediaplayer\vlc.exe' *output*'
383 |
384 |
385 |
386 | # Examples
387 |
388 | Save 1 hour of the stream from 04:55AM to 05:55AM on May 5, 2020, at 720p, to specified folder:
389 | > yrewind -u=oJUvTVdTMyY -s=20200505:0455 -r=720 -o='d:\Streams\'
390 |
391 | Save 89 minutes 30 seconds of the stream from today 10:45AM to 12:15PM, at 1080p:
392 | > yrewind -u=oJUvTVdTMyY -s=T:1045 -d=89.30
393 |
394 | Record livestream until it ends, starting from the beginning, in `.ts` format, save result video to desktop:
395 | > yrewind -u=oJUvTVdTMyY -s=b -o=%UserProfile%\Desktop\.ts -e=*getnext*
396 |
397 | Immediately play (without downloading) with assotiated mediaplayer, from yesterday 03:00AM, at the maximum available resolution:
398 | > yrewind -u=oJUvTVdTMyY -s=Y:0300 -r=max -o=.m3u -e=*output*
399 |
400 | Wait for a new livestream on the specified channel, then start recording from the 30th minute:
401 | > yrewind -u=https://www.youtube.com/c/SkyNews -s=+30
402 |
403 | Batch file example (runs 3 copies of the program at the same time):
404 |
405 | @echo off
406 | set O='d:\Streams\'
407 | set B='c:\Program Files\Chrome\chrome.exe'
408 | set F='c:\Program Files\FFmpeg\'
409 | set Y='c:\Program Files\ee.Yrewind\'
410 | cd /d %Y%
411 |
412 | set U=oJUvTVdTMyY
413 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1500
414 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1600
415 | start yrewind -f=%F% -b=%B% -o=%O% -u=%U% -s=Y:1700
416 |
417 |
418 |
419 | # Notes
420 |
421 | * Loss of packets on the streamer side causes the estimated time to shift. The offset is usually seconds, but if its internet connection is unstable and/or the stream has been running for a long time, it can be minutes or even hours. For example, if the stream was interrupted for a total of 1 hour, then 24-hour frames will be presented as 23-hour. Thus, start point time accuracy can only be guaranteed for the current moment. The further the livestream is rewound, the less accuracy. Also, if there are interruptions in the livestream at the specified time interval, the duration of the saved file will be shorter by the total duration of those interruptions; a warning for this incompleted file will be displayed
422 | * Occasionally, the message `unable to verify the saved file is correct` appears. The reasons may be as follows: if the duration of the saved file cannot be verified (there is a possibility that the file is damaged); if the duration of the saved file does not match the requested one (also in this case, the output file name contains the word *INCOMPLETE*); if the starting point of the requested time interval cannot be accurately determined (for example due to server side error)
423 | * To reduce the chance of output file corruption, it's better not to use Moov Atom formats (`.3gp`, `.mov`, `.mp4`, `.m4a`) for long recordings. Also, these formats don't allow to play a file that is in the process of downloading (other formats do)
424 | * Recently finished livestream can be downloaded within approximately 6 hours of its completion. After this time, such stream 'turns' into a regular video and can be downloaded, for example, using Youtube-dl
425 | * When using proxy, VPN or special firewall settings, keep in mind that not only Yrewind should have appropriate access, but also FFmpeg
426 |
427 |
428 |
429 | # Terms of use
430 |
431 | * This software provides access to information on the Internet that is publicly available and does not require authorization or authentication
432 | * This software is free for non-commercial use and is provided 'as is' without warranty of any kind, either express or implied
433 | * The author will not be liable for data loss, damages or any other kind of loss while using or misusing this software
434 | * The author will not be liable for the misuse of content obtained using this software, including copyrighted, age-restricted, or any other protected content
435 |
436 |
437 |
438 | # Developer info
439 |
440 | * C#
441 | * .NET Framework 4.8
442 | * Visual Studio Community 2022
443 |
444 |
445 |
446 | # Requirements
447 |
448 | * FFmpeg static build (included in the archive)
449 | * Windows 7 and on / Windows Server 2008 R2 and on
450 | * Chromium-based browser
451 |
452 |
453 |
454 | # Tested configuration
455 |
456 | * FFmpeg 4.3 x86 (by Zeranoe)
457 | * Windows 10 Pro x32
458 | * Windows 10 Pro x64
459 |
460 |
461 |
462 | # About
463 |
464 | Console utility for saving YouTube live streams with rewind function up to 167 hours
465 |
466 |
467 |
468 | # Tags
469 |
470 | download downloader dvr live livestream record rewind save stream youtube
471 |
472 | ---
473 | [program page](https://rytsikau.github.io/ee.Yrewind) [start page](https://rytsikau.github.io) [author e-mail](y.rytsikau@gmail.com)
474 | ";
475 | #endregion
476 |
477 | #region [misc] - Paths, URLs, internal presets, etc.
478 |
479 | // Program info
480 | public const string Name = "ee.Yrewind";
481 | public const int BuildDate = 240622;
482 | public static readonly string Version =
483 | Assembly.GetExecutingAssembly().GetName().Version.ToString();
484 |
485 | // Random substring for temp files naming
486 | public static readonly string RandomString =
487 | Guid.NewGuid().ToString().Replace("-", "").Replace("+", "").Substring(0, 7);
488 |
489 | // Cache file path
490 | public static readonly string PathCache =
491 | Path.GetTempPath() + Constants.Name.ToLower() + "_" + Constants.Version + ".cache";
492 |
493 | // Browser netlog path
494 | public static readonly string PathNetlog =
495 | Path.GetTempPath() + Constants.Name.ToLower() + "_" + Constants.RandomString + ".netlog";
496 |
497 | // URL of one-line file with info about latest release, content like:
498 | // 240622;https://github.com/rytsikau/ee.Yrewind/releases/download/20240622/ee.yrewind_24.061.zip
499 | public const string UrlUpdate =
500 | "https://raw.githubusercontent.com/rytsikau/ee.Yrewind/main/_updated.log";
501 |
502 | // Other URLs
503 | public const string UrlProxy = "http://localhost:7799/";
504 | public const string UrlMain = "https://www.youtube.com/";
505 | public const string UrlStream = "https://www.youtube.com/watch?v=[stream_id]";
506 | public const string UrlStreamCover = "https://img.youtube.com/vi/[stream_id]/0.jpg";
507 | public const string UrlStreamOembed =
508 | "https://www.youtube.com/oembed?url=http://youtube.com/watch?v=[stream_id]";
509 | public const string UrlChannel = "https://www.youtube.com/channel/[channel_id]";
510 | public const string UrlChannelCheck =
511 | "https://www.youtube.com/feeds/videos.xml?channel_id=[channel_id]";
512 |
513 | // Other constants
514 | public const int DurationMin = 1;
515 | public const int DurationMax = 18000;
516 | public const int DurationDefault = 3600;
517 | public const int ResolutionMin = 1;
518 | public const int ResolutionMax = 9999;
519 | public const int ResolutionDefault = 1080;
520 | public const int RealTimeBuffer = 60;
521 | public const int FfmpegConsoleWidthMin = 105;
522 | public const int FfmpegTimeout = 120;
523 | public const int CacheShelflifeMinutes = 100;
524 | public const int RewindMaxHours = 167;
525 | public const string BrowserDefault = "msedge";
526 | public const string OutputDirDefault = "saved_streams";
527 | public const string OutputExtDefault = ".mkv";
528 | public const string StartBeginning = "20010101:000000";
529 |
530 | #endregion
531 | }
532 | }
--------------------------------------------------------------------------------
/source/Preparer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Runtime.Serialization.Json;
7 | using System.Text;
8 | using System.Text.RegularExpressions;
9 | using System.Web;
10 | using System.Xml;
11 | using System.Xml.Linq;
12 | using System.Xml.XPath;
13 |
14 | namespace yrewind
15 | {
16 | // Various preparations based on command line request and technical info about the stream
17 | class Preparer
18 | {
19 | // Author name / Channel title
20 | public static string Author { get; private set; }
21 |
22 | // Stream title
23 | public static string Title { get; private set; }
24 |
25 | // Available resolutions
26 | public static string Resolutions { get; private set; }
27 |
28 | // Stream start point
29 | // (for ongoing streams this point assumes no breaks caused by network errors,
30 | // so it may be later than the actual/declared stream start)
31 | public static DateTime IdStart { get; private set; }
32 |
33 | // Start and stop timestamps (used for finished streams)
34 | public static DateTime IdStartTimeStamp { get; private set; }
35 | public static DateTime IdStopTimeStamp { get; private set; }
36 |
37 | // Sequence duration
38 | public static int SeqDuration { get; private set; }
39 |
40 | // Duration to download
41 | public static int Duration { get; private set; }
42 |
43 | // Resolution to download
44 | public static int Resolution { get; private set; }
45 |
46 | // Start point to download
47 | public static DateTime Start { get; private set; }
48 |
49 | // Start sequence to download
50 | public static int StartSeq { get; private set; }
51 |
52 | // Direct URLs of current sequences
53 | public static string UriAdirect { get; private set; }
54 | public static string UriVdirect { get; private set; }
55 |
56 | // Other variables
57 | string hlsManifestUrl;
58 | int currentSeq = -1;
59 | DateTime currentSeqUtc;
60 |
61 | #region Common - Main method of the class
62 | public int Common()
63 | {
64 | int code;
65 | Author = string.Empty;
66 | Title = string.Empty;
67 | Resolutions = string.Empty;
68 | UriAdirect = string.Empty;
69 | UriVdirect = string.Empty;
70 |
71 | if (!string.IsNullOrEmpty(Waiter.UriAdirect) & !string.IsNullOrEmpty(Waiter.UriVdirect))
72 | {
73 | ParseBrowserUris();
74 | }
75 |
76 | ParseHtmlJson();
77 |
78 | if (Author == string.Empty || Title == string.Empty)
79 | {
80 | code = GetInfoWithOembed();
81 | if (code != 0) return code;
82 | }
83 |
84 | GetInfoWithAsegment();
85 |
86 | if (!string.IsNullOrEmpty(hlsManifestUrl) &&
87 | (SeqDuration == default || IdStart == default)) GetInfoWithHls();
88 |
89 | if (Validator.Log)
90 | {
91 | var logInfo =
92 | "\nPreparer 1:" +
93 | "\nAuthor: " + Author +
94 | "\nDuration: " + Duration +
95 | "\nIdStart: " + IdStart +
96 | "\nIdStartTimeStamp: " + IdStartTimeStamp +
97 | "\nIdStopTimeStamp: " + IdStopTimeStamp +
98 | "\nResolution: " + Resolution +
99 | "\nResolutions: " + Resolutions +
100 | "\nSeqDuration: " + SeqDuration +
101 | "\nStart: " + Start +
102 | "\nStartSeq: " + StartSeq +
103 | "\nTitle: " + Title +
104 | "\nUriAdirect: " + UriAdirect +
105 | "\nUriVdirect: " + UriVdirect;
106 | Program.Log(logInfo);
107 | }
108 |
109 | // Just in case, check again
110 | if (string.IsNullOrEmpty(Author) ||
111 | string.IsNullOrEmpty(Title) ||
112 | string.IsNullOrEmpty(Resolutions) ||
113 | string.IsNullOrEmpty(UriAdirect) ||
114 | string.IsNullOrEmpty(UriVdirect) ||
115 | SeqDuration == default ||
116 | IdStart == default)
117 | {
118 | // "Cannot get live stream information"
119 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
120 | return 9310;
121 | }
122 |
123 | code = GetDuration();
124 | if (code != 0) return code;
125 |
126 | code = GetResolution();
127 | if (code != 0) return code;
128 |
129 | if (Resolution > 0)
130 | {
131 | code = CorrectUriVdirect();
132 | if (code != 0) return code;
133 | }
134 |
135 | code = FindStart();
136 | if (code != 0) return code;
137 |
138 | if (Validator.Log)
139 | {
140 | var logInfo =
141 | "\nPreparer 2:" +
142 | "\nAuthor: " + Author +
143 | "\nDuration: " + Duration +
144 | "\nIdStart: " + IdStart +
145 | "\nIdStartTimeStamp: " + IdStartTimeStamp +
146 | "\nIdStopTimeStamp: " + IdStopTimeStamp +
147 | "\nResolution: " + Resolution +
148 | "\nResolutions: " + Resolutions +
149 | "\nSeqDuration: " + SeqDuration +
150 | "\nStart: " + Start +
151 | "\nStartSeq: " + StartSeq +
152 | "\nTitle: " + Title +
153 | "\nUriAdirect: " + UriAdirect +
154 | "\nUriVdirect: " + UriVdirect;
155 | Program.Log(logInfo);
156 | }
157 |
158 | return 0;
159 | }
160 | #endregion
161 |
162 | #region ParseBrowserUris - Get [Resolutions], [UriAdirect], [UriVdirect] from browser netlog
163 | int ParseBrowserUris()
164 | {
165 | // Get [UriAdirect] and [UriVdirect]
166 | UriAdirect = Regex.Replace(Waiter.UriAdirect, @"&sq=\d+", "");
167 | UriVdirect = Regex.Replace(Waiter.UriVdirect, @"&sq=\d+", "");
168 |
169 | // Get Resolutions from [UriVdirect]
170 | try
171 | {
172 | var resolutions = new ArrayList();
173 | var itags = Regex.Match(UriVdirect, @".*&aitags=([^&]+).*").Groups[1].Value;
174 |
175 | foreach (var itag in itags.Replace("%2C", ",").Split(','))
176 | {
177 | if (!int.TryParse(itag, out var itagNumber)) continue;
178 | if (!Constants.Itag.TryGetValue(itagNumber, out var value)) continue;
179 | var resolutionStr = value.Split(';')[2];
180 | if (resolutionStr.Contains("p"))
181 | {
182 | var resolution = int.Parse(resolutionStr.Split('p')[0]);
183 | if (!resolutions.Contains(resolution)) resolutions.Add(resolution);
184 | }
185 | }
186 | resolutions.Sort();
187 | resolutions.Reverse();
188 | Resolutions = string.Join(",", resolutions.ToArray());
189 | }
190 | catch (Exception e)
191 | {
192 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
193 | if (Validator.Log) Program.Log(Program.ErrInfo);
194 |
195 | // "Cannot get live stream information with browser"
196 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
197 | return 9311;
198 | }
199 |
200 | return 0;
201 | }
202 | #endregion
203 |
204 | #region ParseHtmlJson - Get various values from HTML JSON
205 | void ParseHtmlJson()
206 | {
207 | if (Author == string.Empty)
208 | {
209 | try
210 | {
211 | Author = Waiter.JsonHtml.XPathSelectElement("//author").Value;
212 | Author = Program.Replace_InvalidChars(Author);
213 | }
214 | catch (Exception e)
215 | {
216 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
217 | if (Validator.Log) Program.Log(Program.ErrInfo);
218 | }
219 | }
220 |
221 | if (Title == string.Empty)
222 | {
223 | try
224 | {
225 | Title = Waiter.JsonHtml.XPathSelectElement("//title").Value;
226 | Title = Program.Replace_InvalidChars(Title);
227 | }
228 | catch (Exception e)
229 | {
230 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
231 | if (Validator.Log) Program.Log(Program.ErrInfo);
232 | }
233 |
234 | }
235 |
236 | if (Resolutions == string.Empty)
237 | {
238 | try
239 | {
240 | var tmp = Waiter.JsonHtml.XPathSelectElements
241 | ("//adaptiveFormats/*/height").Select(x => x.Value).Distinct();
242 | Resolutions = string.Join(",", tmp);
243 | }
244 | catch (Exception e)
245 | {
246 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
247 | if (Validator.Log) Program.Log(Program.ErrInfo);
248 | }
249 |
250 | }
251 |
252 | if (IdStartTimeStamp == default)
253 | {
254 | try
255 | {
256 | var tmp = Waiter.JsonHtml.XPathSelectElement("//startTimestamp").Value;
257 | tmp = tmp.Replace(" 00:00", "Z");
258 | IdStartTimeStamp = DateTime.Parse(tmp).ToLocalTime();
259 | }
260 | catch (Exception e)
261 | {
262 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
263 | if (Validator.Log) Program.Log(Program.ErrInfo);
264 | }
265 |
266 | }
267 |
268 | if (IdStopTimeStamp == default)
269 | {
270 | try
271 | {
272 | var tmp = Waiter.JsonHtml.XPathSelectElement("//endTimestamp").Value;
273 | tmp = tmp.Replace(" 00:00", "Z");
274 | IdStopTimeStamp = DateTime.Parse(tmp).ToLocalTime();
275 | }
276 | catch (Exception e)
277 | {
278 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
279 | if (Validator.Log) Program.Log(Program.ErrInfo);
280 | }
281 |
282 | }
283 |
284 | if (SeqDuration == default)
285 | {
286 | try
287 | {
288 | SeqDuration =
289 | int.Parse(Waiter.JsonHtml.XPathSelectElement("//targetDurationSec").Value);
290 | }
291 | catch (Exception e)
292 | {
293 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
294 | if (Validator.Log) Program.Log(Program.ErrInfo);
295 | }
296 |
297 | }
298 |
299 | if (UriAdirect == string.Empty)
300 | {
301 | try
302 | {
303 | UriAdirect = Waiter.JsonHtml.XPathSelectElement
304 | ("//adaptiveFormats/*/url[contains(text(),'mime=audio')]").Value;
305 | }
306 | catch (Exception e)
307 | {
308 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
309 | if (Validator.Log) Program.Log(Program.ErrInfo);
310 | }
311 |
312 | }
313 |
314 | if (UriVdirect == string.Empty)
315 | {
316 | try
317 | {
318 | UriVdirect = Waiter.JsonHtml.XPathSelectElement
319 | ("//adaptiveFormats/*/url[contains(text(),'mime=video')]").Value;
320 | }
321 | catch (Exception e)
322 | {
323 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
324 | if (Validator.Log) Program.Log(Program.ErrInfo);
325 | }
326 |
327 | }
328 |
329 | if (hlsManifestUrl == string.Empty)
330 | {
331 | try
332 | {
333 | hlsManifestUrl = Waiter.JsonHtml.XPathSelectElement("//hlsManifestUrl").Value;
334 | }
335 | catch (Exception e)
336 | {
337 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
338 | if (Validator.Log) Program.Log(Program.ErrInfo);
339 | }
340 | }
341 | }
342 | #endregion
343 |
344 | #region GetInfoWithOembed - Get [Author] and [Title] from oembed page
345 | int GetInfoWithOembed()
346 | {
347 | var content = string.Empty;
348 | XElement jsonOembed;
349 |
350 | try
351 | {
352 | var uri = Constants.UrlStreamOembed.Replace("[stream_id]", Waiter.Id);
353 |
354 | using (var wc = new WebClient())
355 | {
356 | content = wc.DownloadString(new Uri(uri));
357 | }
358 | }
359 | catch (WebException e)
360 | {
361 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
362 | if (Validator.Log) Program.Log(Program.ErrInfo);
363 |
364 | // "Cannot get live stream information"
365 | return 9310;
366 | }
367 |
368 | if (Validator.Log) Program.Log(content, "oembed");
369 |
370 | try
371 | {
372 | var tmp1 = Encoding.UTF8.GetBytes(HttpUtility.UrlDecode(content));
373 | var tmp2 = JsonReaderWriterFactory
374 | .CreateJsonReader(tmp1, new XmlDictionaryReaderQuotas());
375 | jsonOembed = XElement.Load(tmp2);
376 | }
377 | catch (Exception e)
378 | {
379 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
380 | if (Validator.Log) Program.Log(Program.ErrInfo);
381 |
382 | // "Cannot get live stream information"
383 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
384 | return 9310;
385 | }
386 |
387 | try
388 | {
389 | Author = jsonOembed.XPathSelectElement("//author_name").Value;
390 | Author = Program.Replace_InvalidChars(Author);
391 |
392 | Title = jsonOembed.XPathSelectElement("//title").Value;
393 | Title = Program.Replace_InvalidChars(Title);
394 | }
395 | catch (Exception e)
396 | {
397 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
398 | if (Validator.Log) Program.Log(Program.ErrInfo);
399 |
400 | // "Cannot get live stream information"
401 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
402 | return 9310;
403 | }
404 |
405 | return 0;
406 | }
407 | #endregion
408 |
409 | #region GetInfoWithAsegment - Get [SeqDuration] and [IdStart] from audio segment
410 | void GetInfoWithAsegment()
411 | {
412 | string content;
413 |
414 | try
415 | {
416 | using (var wc = new WebClient())
417 | {
418 | content = wc.DownloadString(new Uri(UriAdirect));
419 | }
420 | }
421 | catch (WebException e)
422 | {
423 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
424 | if (Validator.Log) Program.Log(Program.ErrInfo);
425 |
426 | return;
427 | }
428 |
429 | if (Validator.Log) Program.Log(content, "segmentA");
430 |
431 | // Parse audio segment as text - get sequence number, its UTC time and duration
432 | if (SeqDuration == default)
433 | {
434 | try
435 | {
436 | var tmp = Regex.Match(content, @".*Target-Duration-Us: (\d+).*")
437 | .Groups[1].Value;
438 | SeqDuration = int.Parse(tmp) / 1000000;
439 | }
440 | catch (Exception e)
441 | {
442 | Program.ErrInfo =
443 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
444 | if (Validator.Log) Program.Log(Program.ErrInfo);
445 |
446 | SeqDuration = default;
447 | }
448 | }
449 | if (currentSeq == -1)
450 | {
451 | try
452 | {
453 | // For finished streams it's number of last sequence
454 | var tmp = Regex.Match(content, @".*Sequence-Number: (\d+).*")
455 | .Groups[1].Value;
456 | currentSeq = int.Parse(tmp);
457 | }
458 | catch (Exception e)
459 | {
460 | Program.ErrInfo =
461 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
462 | if (Validator.Log) Program.Log(Program.ErrInfo);
463 |
464 | currentSeq = -1;
465 | }
466 | }
467 | if (currentSeqUtc == default)
468 | {
469 | try
470 | {
471 | // For finished streams it's time of last sequence
472 | var tmp = Regex.Match(content, @".*Ingestion-Walltime-Us: (\d+).*")
473 | .Groups[1].Value;
474 | currentSeqUtc = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
475 | currentSeqUtc = currentSeqUtc.AddSeconds(long.Parse(tmp) / 1000000);
476 | }
477 | catch (Exception e)
478 | {
479 | Program.ErrInfo =
480 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
481 | if (Validator.Log) Program.Log(Program.ErrInfo);
482 |
483 | currentSeqUtc = default;
484 | }
485 | }
486 |
487 | if (SeqDuration == default || currentSeq == -1 || currentSeqUtc == default)
488 | {
489 | return;
490 | }
491 |
492 | IdStart = currentSeqUtc.AddSeconds(-currentSeq * SeqDuration).ToLocalTime();
493 | }
494 | #endregion
495 |
496 | #region GetInfoWithHls - Get [SeqDuration] and [IdStart] from HLS manifest and playlist
497 | void GetInfoWithHls()
498 | {
499 | string content;
500 | string hlsPlaylistUrl = default;
501 |
502 | // Download HLS manifest
503 | try
504 | {
505 | using (var wc = new WebClient())
506 | {
507 | content = wc.DownloadString(new Uri(hlsManifestUrl));
508 | }
509 | }
510 | catch (WebException e)
511 | {
512 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
513 | if (Validator.Log) Program.Log(Program.ErrInfo);
514 |
515 | return;
516 | }
517 |
518 | if (Validator.Log) Program.Log(content, "m3u8_1");
519 |
520 | // Find HLS playlist URL in the manifest
521 | foreach (var line in content.Split('\n'))
522 | {
523 | if (Regex.IsMatch(line, @"^https.+m3u8$"))
524 | {
525 | hlsPlaylistUrl = line;
526 | break;
527 | }
528 | }
529 |
530 | // Download HLS playlist
531 | try
532 | {
533 | using (var wc = new WebClient())
534 | {
535 | content = wc.DownloadString(new Uri(hlsPlaylistUrl));
536 | }
537 | }
538 | catch (WebException e)
539 | {
540 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
541 | if (Validator.Log) Program.Log(Program.ErrInfo);
542 |
543 | return;
544 | }
545 |
546 | if (Validator.Log) Program.Log(content, "m3u8_2");
547 |
548 | // Parse HLS playlist
549 | if (SeqDuration == default)
550 | {
551 | try
552 | {
553 | var tmp = Regex.Match(content, @".*#EXT-X-TARGETDURATION:(\d+).*")
554 | .Groups[1].Value;
555 | SeqDuration = int.Parse(tmp);
556 | }
557 | catch (Exception e)
558 | {
559 | Program.ErrInfo =
560 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
561 | if (Validator.Log) Program.Log(Program.ErrInfo);
562 |
563 | SeqDuration = default;
564 | }
565 | }
566 | if (currentSeq == -1)
567 | {
568 | try
569 | {
570 | // For finished streams it's number of first sequence
571 | var tmp = Regex.Match(content, @".*#EXT-X-MEDIA-SEQUENCE:(\d+).*")
572 | .Groups[1].Value;
573 | currentSeq = int.Parse(tmp);
574 | }
575 | catch (Exception e)
576 | {
577 | Program.ErrInfo =
578 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
579 | if (Validator.Log) Program.Log(Program.ErrInfo);
580 |
581 | currentSeq = -1;
582 | }
583 | }
584 | if (currentSeqUtc == default)
585 | {
586 | try
587 | {
588 | // For finished streams it's time of first sequence
589 | var tmp = Regex.Match(content, @".*#EXT-X-PROGRAM-DATE-TIME:(.+)\+.*")
590 | .Groups[1].Value;
591 | currentSeqUtc = DateTime.Parse(tmp);
592 | }
593 | catch (Exception e)
594 | {
595 | Program.ErrInfo =
596 | new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
597 | if (Validator.Log) Program.Log(Program.ErrInfo);
598 |
599 | currentSeqUtc = default;
600 | }
601 | }
602 |
603 | if (SeqDuration == default || currentSeq == -1 || currentSeqUtc == default)
604 | {
605 | return;
606 | }
607 |
608 | // Sometimes HLS playlist contains incorrect UTC time of the current sequence.
609 | // Fixing this, we cannot demand an exact match, because the stream is always
610 | // broadcast with delay of several tens of seconds.
611 | // So, we will set an acceptable margin, for example, 3 minutes.
612 | if ((Waiter.IdStatus != Waiter.Stream.Finished) &&
613 | ((DateTime.UtcNow - currentSeqUtc) > TimeSpan.FromMinutes(3)))
614 | {
615 | currentSeqUtc = Program.Start.ToUniversalTime() + Program.Timer.Elapsed;
616 | Program.ResultIsOK = false;
617 | }
618 |
619 | IdStart = currentSeqUtc.AddSeconds(-currentSeq * SeqDuration).ToLocalTime();
620 | }
621 | #endregion
622 |
623 | #region GetDuration - Determine [Duration]
624 | int GetDuration()
625 | {
626 | double numberOfSequences;
627 |
628 | if (Waiter.IdStatus == Waiter.Stream.Finished)
629 | {
630 | numberOfSequences =
631 | (IdStopTimeStamp - IdStartTimeStamp).TotalSeconds / SeqDuration;
632 | }
633 | else
634 | {
635 | numberOfSequences =
636 | double.Parse(Validator.Duration) / SeqDuration;
637 | }
638 |
639 | // Round up to the greater number of sequences
640 | Duration = (int)Math.Ceiling(numberOfSequences) * SeqDuration;
641 | if (Duration > Constants.DurationMax) Duration -= SeqDuration;
642 |
643 | return 0;
644 | }
645 | #endregion
646 |
647 | #region GetResolution - Determine [Resolution] to download
648 | int GetResolution()
649 | {
650 | // To save audio only
651 | if ((Validator.OutputExt == ".aac") ||
652 | (Validator.OutputExt == ".m4a") ||
653 | (Validator.OutputExt == ".wma") ||
654 | (Validator.Resolution == "0"))
655 | {
656 | Resolution = 0;
657 | return 0;
658 | }
659 |
660 | // Other formats
661 | try
662 | {
663 | var resolutionTmp = 0;
664 |
665 | foreach (var item in Preparer.Resolutions.Split(','))
666 | {
667 | resolutionTmp = int.Parse(item);
668 | if (resolutionTmp > int.Parse(Validator.Resolution)) continue;
669 | else break;
670 | }
671 |
672 | Resolution = resolutionTmp;
673 | }
674 | catch (Exception e)
675 | {
676 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
677 | if (Validator.Log) Program.Log(Program.ErrInfo);
678 |
679 | // "Cannot get live stream information"
680 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
681 | return 9310;
682 | }
683 |
684 | // Resolutions above 1080 are only available in VP9 adaptive format ('webm'),
685 | // but for some media containers, FFmpeg cannot store VP9 and MP4a data together.
686 | // Therefore, we will use for them 1080 even if a higher was requested
687 | if (Resolution > 1080)
688 | {
689 | if ((Validator.OutputExt == ".3gp") ||
690 | (Validator.OutputExt == ".flv") ||
691 | (Validator.OutputExt == ".mov") ||
692 | (Validator.OutputExt == ".ts"))
693 | {
694 | Resolution = 1080;
695 | }
696 | }
697 |
698 | return 0;
699 | }
700 | #endregion
701 |
702 | #region CorrectUriVdirect - Get [UriVdirect] with required 'itag'
703 | int CorrectUriVdirect()
704 | {
705 | // Get [UriVdirect] with the correct 'itag' parameter,
706 | // according to the selected resolution and preferred adaptive format
707 |
708 | var itag = string.Empty;
709 | var itags = Regex.Match(UriVdirect, @".*&aitags=([^&]+).*").Groups[1].Value;
710 | var preferredAdaptiveFormat = "mp4";
711 | if ((Validator.OutputExt == ".asf") ||
712 | (Validator.OutputExt == ".mkv") ||
713 | (Validator.OutputExt == ".wmv"))
714 | {
715 | // For ASF, MKV and WMV containers prefer 'webm' format
716 | preferredAdaptiveFormat = "webm";
717 | }
718 |
719 | try
720 | {
721 | foreach (var item in itags.Replace("%2C", ",").Split(','))
722 | {
723 | if (!int.TryParse(item, out var itemNumber)) continue;
724 | if (!Constants.Itag.TryGetValue(itemNumber, out var itemDescr)) continue;
725 | var itemResolutionStr = itemDescr.Split(';')[2];
726 | var itemResolution = int.Parse(itemResolutionStr.Split('p')[0]);
727 | if (itemResolution == Resolution)
728 | {
729 | itag = item;
730 | if (itemDescr.Contains(preferredAdaptiveFormat)) break;
731 | }
732 | }
733 | }
734 | catch (Exception e)
735 | {
736 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + " - " + e.Message;
737 | if (Validator.Log) Program.Log(Program.ErrInfo);
738 |
739 | // "Cannot get live stream information"
740 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
741 | return 9310;
742 | }
743 |
744 | if (itag != string.Empty)
745 | {
746 | UriVdirect = Regex.Replace(UriVdirect, @"&itag=\d+", @"&itag=" + itag);
747 | }
748 | else
749 | {
750 | // "Cannot get live stream information"
751 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
752 | return 9310;
753 | }
754 |
755 | return 0;
756 | }
757 | #endregion
758 |
759 | #region FindStart - Determine [Start] and [StartSeq]
760 | int FindStart()
761 | {
762 | // Internal using in *getnext* mode
763 | if (Validator.Start.StartsWith("seq"))
764 | {
765 | StartSeq = int.Parse(Validator.Start.Replace("seq", ""));
766 |
767 | if (CheckSeq(StartSeq) == "200")
768 | {
769 | Start = IdStart.AddSeconds(StartSeq * SeqDuration);
770 | return 0;
771 | }
772 | else
773 | {
774 | // "Further parts of the stream are unavailable"
775 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
776 | return 9312;
777 | }
778 | }
779 |
780 | // Determine [Start]
781 | if (string.IsNullOrEmpty(Validator.Start))
782 | {
783 | if (Waiter.IdStatus == Waiter.Stream.Ongoing) Start = Program.Start;
784 | else Start = IdStart;
785 | }
786 | else if (Validator.Start == Constants.StartBeginning)
787 | {
788 | Start = IdStart;
789 | }
790 | else if (Validator.Start.StartsWith("-"))
791 | {
792 | if (Waiter.IdStatus == Waiter.Stream.Upcoming)
793 | {
794 | Start = IdStart;
795 | }
796 | else if (Waiter.IdStatus == Waiter.Stream.Ongoing)
797 | {
798 | Start = Program.Start.AddMinutes(int.Parse(Validator.Start));
799 | if (Start < IdStart) Start = IdStart;
800 | }
801 | else if (Waiter.IdStatus == Waiter.Stream.Finished)
802 | {
803 | Start = IdStart;
804 | }
805 | }
806 | else if (Validator.Start.StartsWith("+"))
807 | {
808 | if (Waiter.IdStatus == Waiter.Stream.Upcoming)
809 | {
810 | Start = IdStart.AddMinutes(int.Parse(Validator.Start));
811 | }
812 | else if (Waiter.IdStatus == Waiter.Stream.Ongoing)
813 | {
814 | Start = Program.Start.AddMinutes(int.Parse(Validator.Start));
815 | }
816 | else if (Waiter.IdStatus == Waiter.Stream.Finished)
817 | {
818 | Start = IdStart.AddMinutes(int.Parse(Validator.Start));
819 | }
820 | }
821 | else
822 | {
823 | Start = DateTime.ParseExact(Validator.Start, "yyyyMMdd:HHmmss", null);
824 |
825 | // Round up to the beginning of the next sequence
826 | if (Start > IdStart)
827 | {
828 | var numberOfSequences = (int)((Start - IdStart).TotalSeconds / SeqDuration);
829 | Start = IdStart.AddSeconds(numberOfSequences * SeqDuration + SeqDuration);
830 | }
831 | }
832 |
833 | // Some checks and corrections
834 | if (Start < IdStart)
835 | {
836 | // "Requested time interval isn't available"
837 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() +
838 | " - requested " + Start + " / stream started " + IdStart;
839 | return 9313;
840 | }
841 | else if (Waiter.IdStatus == Waiter.Stream.Ongoing)
842 | {
843 | // For long ongoing streams, sequences 'aged' 167-168 hours
844 | // become unavailable randomly (not in order), so we trim the unstable interval
845 | var earliestStable = (Program.Start + Program.Timer.Elapsed).AddHours(-Constants.RewindMaxHours);
846 | if (Start < earliestStable)
847 | {
848 | if (Validator.Start == Constants.StartBeginning)
849 | {
850 | Start = earliestStable;
851 | }
852 | else
853 | {
854 | // "Requested time interval isn't available"
855 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() +
856 | " - requested " + Start + " / available " + earliestStable;
857 | return 9313;
858 | }
859 | }
860 | }
861 |
862 | // Determine [StartSeq] (round up to the beginning of the next sequence)
863 | StartSeq = (int)Math.Ceiling((Start - IdStart).TotalSeconds / SeqDuration);
864 |
865 | // If the sequence is not YET available, just return to Program
866 | if ((Program.Start + Program.Timer.Elapsed) <
867 | Start.AddSeconds(Constants.RealTimeBuffer))
868 | {
869 | return 0;
870 | }
871 | else if (CheckSeq(StartSeq) != "200")
872 | {
873 | // "Requested time interval isn't available"
874 | Program.ErrInfo = new StackFrame(0, true).GetFileLineNumber() + "";
875 | return 9313;
876 | }
877 |
878 | return 0;
879 | }
880 | #endregion
881 |
882 | #region CheckSeq - Check availability of the specified sequence
883 | public static string CheckSeq(object arg)
884 | {
885 | // Input argument as