mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-10 06:08:35 +03:00
Compare commits
21
Commits
2021.01.29
...
2021.02.04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c25228e5da | ||
|
|
de6000d913 | ||
|
|
ff88a05cff | ||
|
|
8a784c74d1 | ||
|
|
545cc85d11 | ||
|
|
c10d0213fc | ||
|
|
2181983a0c | ||
|
|
e29663c644 | ||
|
|
9c3fe2ef80 | ||
|
|
b60419c51a | ||
|
|
18590cecdb | ||
|
|
9f888147de | ||
|
|
e8be92f9d6 | ||
|
|
b9d973bef1 | ||
|
|
c55256c5a3 | ||
|
|
82e3f6ebda | ||
|
|
af819c216f | ||
|
|
e3b771a898 | ||
|
|
cac96421d9 | ||
|
|
7c245ce877 | ||
|
|
eabce90175 |
@@ -21,7 +21,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.24. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
||||||
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
||||||
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a broken site support
|
- [ ] I'm reporting a broken site support
|
||||||
- [ ] I've verified that I'm running yt-dlp version **2021.01.24**
|
- [ ] I've verified that I'm running yt-dlp version **2021.01.29**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||||
- [ ] I've searched the bugtracker for similar issues including closed ones
|
- [ ] I've searched the bugtracker for similar issues including closed ones
|
||||||
@@ -44,7 +44,7 @@ Add the `-v` flag to your command line you run youtube-dlc with (`youtube-dlc -v
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] yt-dlp version 2021.01.24
|
[debug] yt-dlp version 2021.01.29
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.24. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/pukkandan/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/pukkandan/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||||
- Search the bugtracker for similar site support requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar site support requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
||||||
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a new site support request
|
- [ ] I'm reporting a new site support request
|
||||||
- [ ] I've verified that I'm running yt-dlp version **2021.01.24**
|
- [ ] I've verified that I'm running yt-dlp version **2021.01.29**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that none of provided URLs violate any copyrights
|
- [ ] I've checked that none of provided URLs violate any copyrights
|
||||||
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.24. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar site feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar site feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
||||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a site feature request
|
- [ ] I'm reporting a site feature request
|
||||||
- [ ] I've verified that I'm running yt-dlp version **2021.01.24**
|
- [ ] I've verified that I'm running yt-dlp version **2021.01.29**
|
||||||
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.24. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
||||||
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
||||||
@@ -30,7 +30,7 @@ Carefully read and work through this check list in order to prevent the most com
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a broken site support issue
|
- [ ] I'm reporting a broken site support issue
|
||||||
- [ ] I've verified that I'm running yt-dlp version **2021.01.24**
|
- [ ] I've verified that I'm running yt-dlp version **2021.01.29**
|
||||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||||
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
||||||
@@ -46,7 +46,7 @@ Add the `-v` flag to your command line you run youtube-dlc with (`youtube-dlc -v
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] yt-dlp version 2021.01.24
|
[debug] yt-dlp version 2021.01.29
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.24. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
||||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I'm reporting a feature request
|
- [ ] I'm reporting a feature request
|
||||||
- [ ] I've verified that I'm running yt-dlp version **2021.01.24**
|
- [ ] I've verified that I'm running yt-dlp version **2021.01.29**
|
||||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Quick Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Core Tests
|
name: Core Test
|
||||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ youtube-dlc
|
|||||||
*.swp
|
*.swp
|
||||||
*.ogg
|
*.ogg
|
||||||
*.opus
|
*.opus
|
||||||
|
*.info.json
|
||||||
|
*.annotations.xml
|
||||||
|
*.description
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
*.conf
|
*.conf
|
||||||
|
|||||||
+22
-2
@@ -8,7 +8,7 @@
|
|||||||
* Change "Merged with youtube-dl" version in Readme.md if needed
|
* Change "Merged with youtube-dl" version in Readme.md if needed
|
||||||
* Commit to master as `Release <version>`
|
* Commit to master as `Release <version>`
|
||||||
* Push to origin/release - build task will now run
|
* Push to origin/release - build task will now run
|
||||||
* Update version.py using devscripts\update-version.py
|
* Update version.py using devscripts\update-version.py (be wary of timezones)
|
||||||
* Run `make issuetemplates`
|
* Run `make issuetemplates`
|
||||||
* Commit to master as `[version] update :ci skip all`
|
* Commit to master as `[version] update :ci skip all`
|
||||||
* Push to origin/master
|
* Push to origin/master
|
||||||
@@ -17,6 +17,26 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.02.04
|
||||||
|
* **Merge youtube-dl:** Upto [2021.02.04.1](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.04.1)
|
||||||
|
* **Date/time formatting in output template:** You can now use [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) to format date/time fields. Example: `%(upload_date>%Y-%m-%d)s`
|
||||||
|
* **Multiple output templates:**
|
||||||
|
* Seperate output templates can be given for the different metadata files by using `-o TYPE:TEMPLATE`
|
||||||
|
* The alowed types are: `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`
|
||||||
|
* [youtube] More metadata extraction for channel/playlist URLs (channel, uploader, thumbnail, tags)
|
||||||
|
* New option `--no-write-playlist-metafiles` to prevent writing playlist metadata files
|
||||||
|
* [audius] Fix extractor
|
||||||
|
* [youtube_live_chat] Fix `parse_yt_initial_data` and add `fragment_retries`
|
||||||
|
* [postprocessor] Raise errors correctly
|
||||||
|
* [metadatafromtitle] Fix bug when extracting data from numeric fields
|
||||||
|
* Fix issue with overwriting files
|
||||||
|
* Fix "Default format spec" appearing in quiet mode
|
||||||
|
* [FormatSort] Allow user to prefer av01 over vp9 (The default is still vp9)
|
||||||
|
* [FormatSort] fix bug where `quality` had more priority than `hasvid`
|
||||||
|
* [pyinst] Automatically detect python architecture and working directory
|
||||||
|
* Strip out internal fields such as `_filename` from infojson
|
||||||
|
|
||||||
|
|
||||||
### 2021.01.29
|
### 2021.01.29
|
||||||
* **Features from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl)**: Co-authored by @animelover1984 and @bbepis
|
* **Features from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl)**: Co-authored by @animelover1984 and @bbepis
|
||||||
* Add `--get-comments`
|
* Add `--get-comments`
|
||||||
@@ -174,7 +194,7 @@
|
|||||||
* Added `--no-ignore-dynamic-mpd`, `--no-allow-dynamic-mpd`, `--allow-dynamic-mpd`, `--youtube-include-hls-manifest`, `--no-youtube-include-hls-manifest`, `--no-youtube-skip-hls-manifest`, `--no-download`, `--no-download-archive`, `--resize-buffer`, `--part`, `--mtime`, `--no-keep-fragments`, `--no-cookies`, `--no-write-annotations`, `--no-write-info-json`, `--no-write-description`, `--no-write-thumbnail`, `--youtube-include-dash-manifest`, `--post-overwrites`, `--no-keep-video`, `--no-embed-subs`, `--no-embed-thumbnail`, `--no-add-metadata`, `--no-include-ads`, `--no-write-sub`, `--no-write-auto-sub`, `--no-playlist-reverse`, `--no-restrict-filenames`, `--youtube-include-dash-manifest`, `--no-format-sort-force`, `--flat-videos`, `--no-list-formats-as-table`, `--no-sponskrub`, `--no-sponskrub-cut`, `--no-sponskrub-force`
|
* Added `--no-ignore-dynamic-mpd`, `--no-allow-dynamic-mpd`, `--allow-dynamic-mpd`, `--youtube-include-hls-manifest`, `--no-youtube-include-hls-manifest`, `--no-youtube-skip-hls-manifest`, `--no-download`, `--no-download-archive`, `--resize-buffer`, `--part`, `--mtime`, `--no-keep-fragments`, `--no-cookies`, `--no-write-annotations`, `--no-write-info-json`, `--no-write-description`, `--no-write-thumbnail`, `--youtube-include-dash-manifest`, `--post-overwrites`, `--no-keep-video`, `--no-embed-subs`, `--no-embed-thumbnail`, `--no-add-metadata`, `--no-include-ads`, `--no-write-sub`, `--no-write-auto-sub`, `--no-playlist-reverse`, `--no-restrict-filenames`, `--youtube-include-dash-manifest`, `--no-format-sort-force`, `--flat-videos`, `--no-list-formats-as-table`, `--no-sponskrub`, `--no-sponskrub-cut`, `--no-sponskrub-force`
|
||||||
* Renamed: `--write-subs`, `--no-write-subs`, `--no-write-auto-subs`, `--write-auto-subs`. Note that these can still be used without the ending "s"
|
* Renamed: `--write-subs`, `--no-write-subs`, `--no-write-auto-subs`, `--write-auto-subs`. Note that these can still be used without the ending "s"
|
||||||
* Relaxed validation for format filters so that any arbitrary field can be used
|
* Relaxed validation for format filters so that any arbitrary field can be used
|
||||||
* Fix for embedding thumbnail in mp3 by @pauldubois98
|
* Fix for embedding thumbnail in mp3 by @pauldubois98 ([ytdl-org/youtube-dl#21569](https://github.com/ytdl-org/youtube-dl/pull/21569))
|
||||||
* Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix
|
* Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix
|
||||||
* **Merge youtube-dl:** Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details
|
* **Merge youtube-dl:** Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details
|
||||||
* Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged
|
* Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
|
|
||||||
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection that what is possible by simply using `--format` ([examples](#format-selection-examples))
|
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection that what is possible by simply using `--format` ([examples](#format-selection-examples))
|
||||||
|
|
||||||
* **Merged with youtube-dl v2021.01.24.1**: You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
|
* **Merged with youtube-dl v2021.02.04.1**: You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
|
||||||
|
|
||||||
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, Playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/pukkandan/yt-dlp/pull/31) for details.
|
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, Playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/pukkandan/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
|
|
||||||
* **Plugin support**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugin support**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
* **Multiple paths**: You can give different paths for different types of files. You can also set a temporary path where intermediary files are downloaded to. See [`--paths`](https://github.com/pukkandan/yt-dlp/#:~:text=-P,%20--paths%20TYPE:PATH) for details
|
* **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to. See [`--paths`](https://github.com/pukkandan/yt-dlp/#:~:text=-P,%20--paths%20TYPE:PATH) for details
|
||||||
|
|
||||||
<!-- Relative link doesn't work for "#:~:text=" -->
|
<!-- Relative link doesn't work for "#:~:text=" -->
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
|
|
||||||
* **Other new options**: `--parse-metadata`, `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
* **Other new options**: `--parse-metadata`, `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
||||||
|
|
||||||
* **Improvements**: Multiple `--postprocessor-args` and `--external-downloader-args`, `%(duration_string)s` in `-o`, faster archive checking, more [format selection options](#format-selection) etc
|
* **Improvements**: Multiple `--postprocessor-args` and `--external-downloader-args`, Date/time formatting in `-o`, faster archive checking, more [format selection options](#format-selection) etc
|
||||||
|
|
||||||
|
|
||||||
See [changelog](Changelog.md) or [commits](https://github.com/pukkandan/yt-dlp/commits) for the full list of changes
|
See [changelog](Changelog.md) or [commits](https://github.com/pukkandan/yt-dlp/commits) for the full list of changes
|
||||||
@@ -106,7 +106,7 @@ To build the Windows executable, you must have pyinstaller (and optionally mutag
|
|||||||
|
|
||||||
python -m pip install --upgrade pyinstaller mutagen
|
python -m pip install --upgrade pyinstaller mutagen
|
||||||
|
|
||||||
For the 64bit version, run `py devscripts\pyinst.py 64` using 64bit python3. Similarly, to install 32bit version, run `py devscripts\pyinst.py 32` using 32bit python (preferably 3)
|
Once you have all the necessary dependancies installed, just run `py devscripts\pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it. It is strongly reccomended to use python3 although python2.6+ is supported.
|
||||||
|
|
||||||
You can also build the executable without any version info or metadata by using:
|
You can also build the executable without any version info or metadata by using:
|
||||||
|
|
||||||
@@ -333,16 +333,16 @@ Then simply type this
|
|||||||
comments and ignored
|
comments and ignored
|
||||||
-P, --paths TYPE:PATH The paths where the files should be
|
-P, --paths TYPE:PATH The paths where the files should be
|
||||||
downloaded. Specify the type of file and
|
downloaded. Specify the type of file and
|
||||||
the path separated by a colon ":"
|
the path separated by a colon ":". All the
|
||||||
(supported: description|annotation|subtitle
|
same types as --output are supported.
|
||||||
|infojson|thumbnail). Additionally, you can
|
Additionally, you can also provide "home"
|
||||||
also provide "home" and "temp" paths. All
|
and "temp" paths. All intermediary files
|
||||||
intermediary files are first downloaded to
|
are first downloaded to the temp path and
|
||||||
the temp path and then the final files are
|
then the final files are moved over to the
|
||||||
moved over to the home path after download
|
home path after download is finished. This
|
||||||
is finished. Note that this option is
|
option is ignored if --output is an
|
||||||
ignored if --output is an absolute path
|
absolute path
|
||||||
-o, --output TEMPLATE Output filename template, see "OUTPUT
|
-o, --output [TYPE:]TEMPLATE Output filename template, see "OUTPUT
|
||||||
TEMPLATE" for details
|
TEMPLATE" for details
|
||||||
--output-na-placeholder TEXT Placeholder value for unavailable meta
|
--output-na-placeholder TEXT Placeholder value for unavailable meta
|
||||||
fields in output filename template
|
fields in output filename template
|
||||||
@@ -359,9 +359,11 @@ Then simply type this
|
|||||||
This option includes --no-continue
|
This option includes --no-continue
|
||||||
--no-force-overwrites Do not overwrite the video, but overwrite
|
--no-force-overwrites Do not overwrite the video, but overwrite
|
||||||
related files (default)
|
related files (default)
|
||||||
-c, --continue Resume partially downloaded files (default)
|
-c, --continue Resume partially downloaded files/fragments
|
||||||
--no-continue Restart download of partially downloaded
|
(default)
|
||||||
files from beginning
|
--no-continue Do not resume partially downloaded
|
||||||
|
fragments. If the file is unfragmented,
|
||||||
|
restart download of the entire file
|
||||||
--part Use .part files instead of writing directly
|
--part Use .part files instead of writing directly
|
||||||
into output file (default)
|
into output file (default)
|
||||||
--no-part Do not use .part files - write directly
|
--no-part Do not use .part files - write directly
|
||||||
@@ -374,10 +376,16 @@ Then simply type this
|
|||||||
file
|
file
|
||||||
--no-write-description Do not write video description (default)
|
--no-write-description Do not write video description (default)
|
||||||
--write-info-json Write video metadata to a .info.json file
|
--write-info-json Write video metadata to a .info.json file
|
||||||
|
(this may contain personal information)
|
||||||
--no-write-info-json Do not write video metadata (default)
|
--no-write-info-json Do not write video metadata (default)
|
||||||
--write-annotations Write video annotations to a
|
--write-annotations Write video annotations to a
|
||||||
.annotations.xml file
|
.annotations.xml file
|
||||||
--no-write-annotations Do not write video annotations (default)
|
--no-write-annotations Do not write video annotations (default)
|
||||||
|
--write-playlist-metafiles Write playlist metadata in addition to the
|
||||||
|
video metadata when using --write-info-json,
|
||||||
|
--write-description etc. (default)
|
||||||
|
--no-write-playlist-metafiles Do not write playlist metadata when using
|
||||||
|
--write-info-json, --write-description etc.
|
||||||
--get-comments Retrieve video comments to be placed in the
|
--get-comments Retrieve video comments to be placed in the
|
||||||
.info.json file
|
.info.json file
|
||||||
--load-info-json FILE JSON file containing the video information
|
--load-info-json FILE JSON file containing the video information
|
||||||
@@ -746,7 +754,11 @@ The `-o` option is used to indicate a template for the output file names while `
|
|||||||
|
|
||||||
**tl;dr:** [navigate me to examples](#output-template-examples).
|
**tl;dr:** [navigate me to examples](#output-template-examples).
|
||||||
|
|
||||||
The basic usage is not to set any template arguments when downloading a single file, like in `youtube-dlc -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Allowed names along with sequence type are:
|
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `youtube-dlc -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
|
||||||
|
|
||||||
|
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||||
|
|
||||||
|
The available fields are:
|
||||||
|
|
||||||
- `id` (string): Video identifier
|
- `id` (string): Video identifier
|
||||||
- `title` (string): Video title
|
- `title` (string): Video title
|
||||||
@@ -853,7 +865,7 @@ If you are using an output template inside a Windows batch file then you must es
|
|||||||
|
|
||||||
#### Output template examples
|
#### Output template examples
|
||||||
|
|
||||||
Note that on Windows you may need to use double quotes instead of single.
|
Note that on Windows you need to use double quotes instead of single.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
$ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
||||||
@@ -865,14 +877,17 @@ youtube-dlc_test_video_.mp4 # A simple file name
|
|||||||
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
||||||
$ youtube-dlc -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
$ youtube-dlc -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
|
|
||||||
|
# Download YouTube playlist videos in seperate directories according to their uploaded year
|
||||||
|
$ youtube-dlc -o '%(upload_date>%Y)s/%(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
|
|
||||||
# Download all playlists of YouTube channel/user keeping each playlist in separate directory:
|
# Download all playlists of YouTube channel/user keeping each playlist in separate directory:
|
||||||
$ youtube-dlc -o '%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/user/TheLinuxFoundation/playlists
|
$ youtube-dlc -o '%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/user/TheLinuxFoundation/playlists
|
||||||
|
|
||||||
# Download Udemy course keeping each chapter in separate directory under MyVideos directory in your home
|
# Download Udemy course keeping each chapter in separate directory under MyVideos directory in your home
|
||||||
$ youtube-dlc -u user -p password -o '~/MyVideos/%(playlist)s/%(chapter_number)s - %(chapter)s/%(title)s.%(ext)s' https://www.udemy.com/java-tutorial/
|
$ youtube-dlc -u user -p password -P '~/MyVideos' -o '%(playlist)s/%(chapter_number)s - %(chapter)s/%(title)s.%(ext)s' https://www.udemy.com/java-tutorial/
|
||||||
|
|
||||||
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
||||||
$ youtube-dlc -o "C:/MyVideos/%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" https://videomore.ru/kino_v_detalayah/5_sezon/367617
|
$ youtube-dlc -P "C:/MyVideos" -o "%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" https://videomore.ru/kino_v_detalayah/5_sezon/367617
|
||||||
|
|
||||||
# Stream the video being downloaded to stdout
|
# Stream the video being downloaded to stdout
|
||||||
$ youtube-dlc -o - BaW_jenozKc
|
$ youtube-dlc -o - BaW_jenozKc
|
||||||
@@ -958,7 +973,7 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
|
|||||||
- `quality`: The quality of the format. This is a metadata field available in some websites
|
- `quality`: The quality of the format. This is a metadata field available in some websites
|
||||||
- `source`: Preference of the source as given by the extractor
|
- `source`: Preference of the source as given by the extractor
|
||||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8-native` > `m3u8` > `http-dash-segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`)
|
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8-native` > `m3u8` > `http-dash-segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`)
|
||||||
- `vcodec`: Video Codec (`vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown)
|
- `vcodec`: Video Codec (`av01` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown)
|
||||||
- `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown)
|
- `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown)
|
||||||
- `codec`: Equivalent to `vcodec,acodec`
|
- `codec`: Equivalent to `vcodec,acodec`
|
||||||
- `vext`: Video Extension (`mp4` > `webm` > `flv` > other > unknown). If `--prefer-free-formats` is used, `webm` is prefered.
|
- `vext`: Video Extension (`mp4` > `webm` > `flv` > other > unknown). If `--prefer-free-formats` is used, `webm` is prefered.
|
||||||
@@ -979,7 +994,7 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
|
|||||||
|
|
||||||
Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in decending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a prefered value for the fields, seperated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two prefered values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in decending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a prefered value for the fields, seperated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two prefered values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
||||||
|
|
||||||
The fields `hasvid`, `ie_pref`, `lang`, `quality` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `res,fps,codec,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order.
|
The fields `hasvid`, `ie_pref`, `lang`, `quality` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `res,fps,codec:vp9,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order.
|
||||||
|
|
||||||
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all repects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all repects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
||||||
|
|
||||||
|
|||||||
+26
-17
@@ -1,5 +1,10 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
from PyInstaller.utils.win32.versioninfo import (
|
from PyInstaller.utils.win32.versioninfo import (
|
||||||
VarStruct, VarFileInfo, StringStruct, StringTable,
|
VarStruct, VarFileInfo, StringStruct, StringTable,
|
||||||
@@ -7,13 +12,17 @@ from PyInstaller.utils.win32.versioninfo import (
|
|||||||
)
|
)
|
||||||
import PyInstaller.__main__
|
import PyInstaller.__main__
|
||||||
|
|
||||||
|
arch = sys.argv[1] if len(sys.argv) > 1 else platform.architecture()[0][:2]
|
||||||
|
assert arch in ('32', '64')
|
||||||
|
print('Building %sbit version' % arch)
|
||||||
|
_x86 = '_x86' if arch == '32' else ''
|
||||||
|
|
||||||
assert len(sys.argv) > 1 and sys.argv[1] in ("32", "64")
|
FILE_DESCRIPTION = 'Media Downloader%s' % (' (32 Bit)' if _x86 else '')
|
||||||
_x86 = "_x86" if sys.argv[1] == "32" else ""
|
SHORT_URLS = {'32': 'git.io/JUGsM', '64': 'git.io/JLh7K'}
|
||||||
|
|
||||||
FILE_DESCRIPTION = 'Media Downloader%s' % (" (32 Bit)" if _x86 else '')
|
|
||||||
SHORT_URLS = {"32": "git.io/JUGsM", "64": "git.io/JLh7K"}
|
|
||||||
|
|
||||||
|
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
print('Changing working directory to %s' % root_dir)
|
||||||
|
os.chdir(root_dir)
|
||||||
|
|
||||||
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
||||||
VERSION = locals()['__version__']
|
VERSION = locals()['__version__']
|
||||||
@@ -38,21 +47,21 @@ VERSION_FILE = VSVersionInfo(
|
|||||||
kids=[
|
kids=[
|
||||||
StringFileInfo([
|
StringFileInfo([
|
||||||
StringTable(
|
StringTable(
|
||||||
"040904B0", [
|
'040904B0', [
|
||||||
StringStruct("Comments", "Youtube-dlc%s Command Line Interface." % _x86),
|
StringStruct('Comments', 'Youtube-dlc%s Command Line Interface.' % _x86),
|
||||||
StringStruct("CompanyName", "pukkandan@gmail.com"),
|
StringStruct('CompanyName', 'pukkandan@gmail.com'),
|
||||||
StringStruct("FileDescription", FILE_DESCRIPTION),
|
StringStruct('FileDescription', FILE_DESCRIPTION),
|
||||||
StringStruct("FileVersion", VERSION),
|
StringStruct('FileVersion', VERSION),
|
||||||
StringStruct("InternalName", "youtube-dlc%s" % _x86),
|
StringStruct('InternalName', 'youtube-dlc%s' % _x86),
|
||||||
StringStruct(
|
StringStruct(
|
||||||
"LegalCopyright",
|
'LegalCopyright',
|
||||||
"pukkandan@gmail.com | UNLICENSE",
|
'pukkandan@gmail.com | UNLICENSE',
|
||||||
),
|
),
|
||||||
StringStruct("OriginalFilename", "youtube-dlc%s.exe" % _x86),
|
StringStruct('OriginalFilename', 'youtube-dlc%s.exe' % _x86),
|
||||||
StringStruct("ProductName", "Youtube-dlc%s" % _x86),
|
StringStruct('ProductName', 'Youtube-dlc%s' % _x86),
|
||||||
StringStruct("ProductVersion", "%s%s | %s" % (VERSION, _x86, SHORT_URLS[sys.argv[1]])),
|
StringStruct('ProductVersion', '%s%s | %s' % (VERSION, _x86, SHORT_URLS[arch])),
|
||||||
])]),
|
])]),
|
||||||
VarFileInfo([VarStruct("Translation", [0, 1200])])
|
VarFileInfo([VarStruct('Translation', [0, 1200])])
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,9 @@
|
|||||||
- **AudioBoom**
|
- **AudioBoom**
|
||||||
- **audiomack**
|
- **audiomack**
|
||||||
- **audiomack:album**
|
- **audiomack:album**
|
||||||
- **Audius**
|
- **Audius**: Audius.co
|
||||||
- **audius:playlist**
|
- **audius:playlist**: Audius.co playlists
|
||||||
|
- **audius:track**: Audius track ID or API link. Prepend with "audius:"
|
||||||
- **AWAAN**
|
- **AWAAN**
|
||||||
- **awaan:live**
|
- **awaan:live**
|
||||||
- **awaan:season**
|
- **awaan:season**
|
||||||
@@ -558,6 +559,7 @@
|
|||||||
- **mtv:video**
|
- **mtv:video**
|
||||||
- **mtvjapan**
|
- **mtvjapan**
|
||||||
- **mtvservices:embedded**
|
- **mtvservices:embedded**
|
||||||
|
- **MTVUutisetArticle**
|
||||||
- **MuenchenTV**: münchen.tv
|
- **MuenchenTV**: münchen.tv
|
||||||
- **mva**: Microsoft Virtual Academy videos
|
- **mva**: Microsoft Virtual Academy videos
|
||||||
- **mva:course**: Microsoft Virtual Academy courses
|
- **mva:course**: Microsoft Virtual Academy courses
|
||||||
@@ -1089,7 +1091,6 @@
|
|||||||
- **vidme**
|
- **vidme**
|
||||||
- **vidme:user**
|
- **vidme:user**
|
||||||
- **vidme:user:likes**
|
- **vidme:user:likes**
|
||||||
- **Vidzi**
|
|
||||||
- **vier**: vier.be and vijf.be
|
- **vier**: vier.be and vijf.be
|
||||||
- **vier:videos**
|
- **vier:videos**
|
||||||
- **viewlift**
|
- **viewlift**
|
||||||
@@ -1134,6 +1135,7 @@
|
|||||||
- **vrv**
|
- **vrv**
|
||||||
- **vrv:series**
|
- **vrv:series**
|
||||||
- **VShare**
|
- **VShare**
|
||||||
|
- **VTM**
|
||||||
- **VTXTV**
|
- **VTXTV**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# Allow direct execution
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from test.helper import expect_value
|
|
||||||
from youtube_dlc.extractor import YoutubeIE
|
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeChapters(unittest.TestCase):
|
|
||||||
|
|
||||||
_TEST_CASES = [
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=A22oy8dFjqc
|
|
||||||
# pattern: 00:00 - <title>
|
|
||||||
'''This is the absolute ULTIMATE experience of Queen's set at LIVE AID, this is the best video mixed to the absolutely superior stereo radio broadcast. This vastly superior audio mix takes a huge dump on all of the official mixes. Best viewed in 1080p. ENJOY! ***MAKE SURE TO READ THE DESCRIPTION***<br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+36);return false;">00:36</a> - Bohemian Rhapsody<br /><a href="#" onclick="yt.www.watch.player.seekTo(02*60+42);return false;">02:42</a> - Radio Ga Ga<br /><a href="#" onclick="yt.www.watch.player.seekTo(06*60+53);return false;">06:53</a> - Ay Oh!<br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+34);return false;">07:34</a> - Hammer To Fall<br /><a href="#" onclick="yt.www.watch.player.seekTo(12*60+08);return false;">12:08</a> - Crazy Little Thing Called Love<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+03);return false;">16:03</a> - We Will Rock You<br /><a href="#" onclick="yt.www.watch.player.seekTo(17*60+18);return false;">17:18</a> - We Are The Champions<br /><a href="#" onclick="yt.www.watch.player.seekTo(21*60+12);return false;">21:12</a> - Is This The World We Created...?<br /><br />Short song analysis:<br /><br />- "Bohemian Rhapsody": Although it's a short medley version, it's one of the best performances of the ballad section, with Freddie nailing the Bb4s with the correct studio phrasing (for the first time ever!).<br /><br />- "Radio Ga Ga": Although it's missing one chorus, this is one of - if not the best - the best versions ever, Freddie nails all the Bb4s and sounds very clean! Spike Edney's Roland Jupiter 8 also really shines through on this mix, compared to the DVD releases!<br /><br />- "Audience Improv": A great improv, Freddie sounds strong and confident. You gotta love when he sustains that A4 for 4 seconds!<br /><br />- "Hammer To Fall": Despite missing a verse and a chorus, it's a strong version (possibly the best ever). Freddie sings the song amazingly, and even ad-libs a C#5 and a C5! Also notice how heavy Brian's guitar sounds compared to the thin DVD mixes - it roars!<br /><br />- "Crazy Little Thing Called Love": A great version, the crowd loves the song, the jam is great as well! Only downside to this is the slight feedback issues.<br /><br />- "We Will Rock You": Although cut down to the 1st verse and chorus, Freddie sounds strong. He nails the A4, and the solo from Dr. May is brilliant!<br /><br />- "We Are the Champions": Perhaps the high-light of the performance - Freddie is very daring on this version, he sustains the pre-chorus Bb4s, nails the 1st C5, belts great A4s, but most importantly: He nails the chorus Bb4s, in all 3 choruses! This is the only time he has ever done so! It has to be said though, the last one sounds a bit rough, but that's a side effect of belting high notes for the past 18 minutes, with nodules AND laryngitis!<br /><br />- "Is This The World We Created... ?": Freddie and Brian perform a beautiful version of this, and it is one of the best versions ever. It's both sad and hilarious that a couple of BBC engineers are talking over the song, one of them being completely oblivious of the fact that he is interrupting the performance, on live television... Which was being televised to almost 2 billion homes.<br /><br /><br />All rights go to their respective owners!<br />-----Copyright Disclaimer Under Section 107 of the Copyright Act 1976, allowance is made for fair use for purposes such as criticism, comment, news reporting, teaching, scholarship, and research. Fair use is a use permitted by copyright statute that might otherwise be infringing. Non-profit, educational or personal use tips the balance in favor of fair use''',
|
|
||||||
1477,
|
|
||||||
[{
|
|
||||||
'start_time': 36,
|
|
||||||
'end_time': 162,
|
|
||||||
'title': 'Bohemian Rhapsody',
|
|
||||||
}, {
|
|
||||||
'start_time': 162,
|
|
||||||
'end_time': 413,
|
|
||||||
'title': 'Radio Ga Ga',
|
|
||||||
}, {
|
|
||||||
'start_time': 413,
|
|
||||||
'end_time': 454,
|
|
||||||
'title': 'Ay Oh!',
|
|
||||||
}, {
|
|
||||||
'start_time': 454,
|
|
||||||
'end_time': 728,
|
|
||||||
'title': 'Hammer To Fall',
|
|
||||||
}, {
|
|
||||||
'start_time': 728,
|
|
||||||
'end_time': 963,
|
|
||||||
'title': 'Crazy Little Thing Called Love',
|
|
||||||
}, {
|
|
||||||
'start_time': 963,
|
|
||||||
'end_time': 1038,
|
|
||||||
'title': 'We Will Rock You',
|
|
||||||
}, {
|
|
||||||
'start_time': 1038,
|
|
||||||
'end_time': 1272,
|
|
||||||
'title': 'We Are The Champions',
|
|
||||||
}, {
|
|
||||||
'start_time': 1272,
|
|
||||||
'end_time': 1477,
|
|
||||||
'title': 'Is This The World We Created...?',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=ekYlRhALiRQ
|
|
||||||
# pattern: <num>. <title> 0:00
|
|
||||||
'1. Those Beaten Paths of Confusion <a href="#" onclick="yt.www.watch.player.seekTo(0*60+00);return false;">0:00</a><br />2. Beyond the Shadows of Emptiness & Nothingness <a href="#" onclick="yt.www.watch.player.seekTo(11*60+47);return false;">11:47</a><br />3. Poison Yourself...With Thought <a href="#" onclick="yt.www.watch.player.seekTo(26*60+30);return false;">26:30</a><br />4. The Agents of Transformation <a href="#" onclick="yt.www.watch.player.seekTo(35*60+57);return false;">35:57</a><br />5. Drowning in the Pain of Consciousness <a href="#" onclick="yt.www.watch.player.seekTo(44*60+32);return false;">44:32</a><br />6. Deny the Disease of Life <a href="#" onclick="yt.www.watch.player.seekTo(53*60+07);return false;">53:07</a><br /><br />More info/Buy: http://crepusculonegro.storenvy.com/products/257645-cn-03-arizmenda-within-the-vacuum-of-infinity<br /><br />No copyright is intended. The rights to this video are assumed by the owner and its affiliates.',
|
|
||||||
4009,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 707,
|
|
||||||
'title': '1. Those Beaten Paths of Confusion',
|
|
||||||
}, {
|
|
||||||
'start_time': 707,
|
|
||||||
'end_time': 1590,
|
|
||||||
'title': '2. Beyond the Shadows of Emptiness & Nothingness',
|
|
||||||
}, {
|
|
||||||
'start_time': 1590,
|
|
||||||
'end_time': 2157,
|
|
||||||
'title': '3. Poison Yourself...With Thought',
|
|
||||||
}, {
|
|
||||||
'start_time': 2157,
|
|
||||||
'end_time': 2672,
|
|
||||||
'title': '4. The Agents of Transformation',
|
|
||||||
}, {
|
|
||||||
'start_time': 2672,
|
|
||||||
'end_time': 3187,
|
|
||||||
'title': '5. Drowning in the Pain of Consciousness',
|
|
||||||
}, {
|
|
||||||
'start_time': 3187,
|
|
||||||
'end_time': 4009,
|
|
||||||
'title': '6. Deny the Disease of Life',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=WjL4pSzog9w
|
|
||||||
# pattern: 00:00 <title>
|
|
||||||
'<a href="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" class="yt-uix-servicelink " data-target-new-window="True" data-servicelink="CDAQ6TgYACITCNf1raqT2dMCFdRjGAod_o0CBSj4HQ" data-url="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" rel="nofollow noopener" target="_blank">https://arizmenda.bandcamp.com/merch/...</a><br /><br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+00);return false;">00:00</a> Christening Unborn Deformities <br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+08);return false;">07:08</a> Taste of Purity<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+16);return false;">16:16</a> Sculpting Sins of a Universal Tongue<br /><a href="#" onclick="yt.www.watch.player.seekTo(24*60+45);return false;">24:45</a> Birth<br /><a href="#" onclick="yt.www.watch.player.seekTo(31*60+24);return false;">31:24</a> Neves<br /><a href="#" onclick="yt.www.watch.player.seekTo(37*60+55);return false;">37:55</a> Libations in Limbo',
|
|
||||||
2705,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 428,
|
|
||||||
'title': 'Christening Unborn Deformities',
|
|
||||||
}, {
|
|
||||||
'start_time': 428,
|
|
||||||
'end_time': 976,
|
|
||||||
'title': 'Taste of Purity',
|
|
||||||
}, {
|
|
||||||
'start_time': 976,
|
|
||||||
'end_time': 1485,
|
|
||||||
'title': 'Sculpting Sins of a Universal Tongue',
|
|
||||||
}, {
|
|
||||||
'start_time': 1485,
|
|
||||||
'end_time': 1884,
|
|
||||||
'title': 'Birth',
|
|
||||||
}, {
|
|
||||||
'start_time': 1884,
|
|
||||||
'end_time': 2275,
|
|
||||||
'title': 'Neves',
|
|
||||||
}, {
|
|
||||||
'start_time': 2275,
|
|
||||||
'end_time': 2705,
|
|
||||||
'title': 'Libations in Limbo',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=o3r1sn-t3is
|
|
||||||
# pattern: <title> 00:00 <note>
|
|
||||||
'Download this show in MP3: <a href="http://sh.st/njZKK" class="yt-uix-servicelink " data-url="http://sh.st/njZKK" data-target-new-window="True" data-servicelink="CDAQ6TgYACITCK3j8_6o2dMCFVDCGAoduVAKKij4HQ" rel="nofollow noopener" target="_blank">http://sh.st/njZKK</a><br /><br />Setlist:<br />I-E-A-I-A-I-O <a href="#" onclick="yt.www.watch.player.seekTo(00*60+45);return false;">00:45</a><br />Suite-Pee <a href="#" onclick="yt.www.watch.player.seekTo(4*60+26);return false;">4:26</a> (Incomplete)<br />Attack <a href="#" onclick="yt.www.watch.player.seekTo(5*60+31);return false;">5:31</a> (First live performance since 2011)<br />Prison Song <a href="#" onclick="yt.www.watch.player.seekTo(8*60+42);return false;">8:42</a><br />Know <a href="#" onclick="yt.www.watch.player.seekTo(12*60+32);return false;">12:32</a> (First live performance since 2011)<br />Aerials <a href="#" onclick="yt.www.watch.player.seekTo(15*60+32);return false;">15:32</a><br />Soldier Side - Intro <a href="#" onclick="yt.www.watch.player.seekTo(19*60+13);return false;">19:13</a><br />B.Y.O.B. <a href="#" onclick="yt.www.watch.player.seekTo(20*60+09);return false;">20:09</a><br />Soil <a href="#" onclick="yt.www.watch.player.seekTo(24*60+32);return false;">24:32</a><br />Darts <a href="#" onclick="yt.www.watch.player.seekTo(27*60+48);return false;">27:48</a><br />Radio/Video <a href="#" onclick="yt.www.watch.player.seekTo(30*60+38);return false;">30:38</a><br />Hypnotize <a href="#" onclick="yt.www.watch.player.seekTo(35*60+05);return false;">35:05</a><br />Temper <a href="#" onclick="yt.www.watch.player.seekTo(38*60+08);return false;">38:08</a> (First live performance since 1999)<br />CUBErt <a href="#" onclick="yt.www.watch.player.seekTo(41*60+00);return false;">41:00</a><br />Needles <a href="#" onclick="yt.www.watch.player.seekTo(42*60+57);return false;">42:57</a><br />Deer Dance <a href="#" onclick="yt.www.watch.player.seekTo(46*60+27);return false;">46:27</a><br />Bounce <a href="#" onclick="yt.www.watch.player.seekTo(49*60+38);return false;">49:38</a><br />Suggestions <a href="#" onclick="yt.www.watch.player.seekTo(51*60+25);return false;">51:25</a><br />Psycho <a href="#" onclick="yt.www.watch.player.seekTo(53*60+52);return false;">53:52</a><br />Chop Suey! <a href="#" onclick="yt.www.watch.player.seekTo(58*60+13);return false;">58:13</a><br />Lonely Day <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+01*60+15);return false;">1:01:15</a><br />Question! <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+04*60+14);return false;">1:04:14</a><br />Lost in Hollywood <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+08*60+10);return false;">1:08:10</a><br />Vicinity of Obscenity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+13*60+40);return false;">1:13:40</a>(First live performance since 2012)<br />Forest <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+16*60+17);return false;">1:16:17</a><br />Cigaro <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+20*60+02);return false;">1:20:02</a><br />Toxicity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+23*60+57);return false;">1:23:57</a>(with Chino Moreno)<br />Sugar <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+27*60+53);return false;">1:27:53</a>',
|
|
||||||
5640,
|
|
||||||
[{
|
|
||||||
'start_time': 45,
|
|
||||||
'end_time': 266,
|
|
||||||
'title': 'I-E-A-I-A-I-O',
|
|
||||||
}, {
|
|
||||||
'start_time': 266,
|
|
||||||
'end_time': 331,
|
|
||||||
'title': 'Suite-Pee (Incomplete)',
|
|
||||||
}, {
|
|
||||||
'start_time': 331,
|
|
||||||
'end_time': 522,
|
|
||||||
'title': 'Attack (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 522,
|
|
||||||
'end_time': 752,
|
|
||||||
'title': 'Prison Song',
|
|
||||||
}, {
|
|
||||||
'start_time': 752,
|
|
||||||
'end_time': 932,
|
|
||||||
'title': 'Know (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 932,
|
|
||||||
'end_time': 1153,
|
|
||||||
'title': 'Aerials',
|
|
||||||
}, {
|
|
||||||
'start_time': 1153,
|
|
||||||
'end_time': 1209,
|
|
||||||
'title': 'Soldier Side - Intro',
|
|
||||||
}, {
|
|
||||||
'start_time': 1209,
|
|
||||||
'end_time': 1472,
|
|
||||||
'title': 'B.Y.O.B.',
|
|
||||||
}, {
|
|
||||||
'start_time': 1472,
|
|
||||||
'end_time': 1668,
|
|
||||||
'title': 'Soil',
|
|
||||||
}, {
|
|
||||||
'start_time': 1668,
|
|
||||||
'end_time': 1838,
|
|
||||||
'title': 'Darts',
|
|
||||||
}, {
|
|
||||||
'start_time': 1838,
|
|
||||||
'end_time': 2105,
|
|
||||||
'title': 'Radio/Video',
|
|
||||||
}, {
|
|
||||||
'start_time': 2105,
|
|
||||||
'end_time': 2288,
|
|
||||||
'title': 'Hypnotize',
|
|
||||||
}, {
|
|
||||||
'start_time': 2288,
|
|
||||||
'end_time': 2460,
|
|
||||||
'title': 'Temper (First live performance since 1999)',
|
|
||||||
}, {
|
|
||||||
'start_time': 2460,
|
|
||||||
'end_time': 2577,
|
|
||||||
'title': 'CUBErt',
|
|
||||||
}, {
|
|
||||||
'start_time': 2577,
|
|
||||||
'end_time': 2787,
|
|
||||||
'title': 'Needles',
|
|
||||||
}, {
|
|
||||||
'start_time': 2787,
|
|
||||||
'end_time': 2978,
|
|
||||||
'title': 'Deer Dance',
|
|
||||||
}, {
|
|
||||||
'start_time': 2978,
|
|
||||||
'end_time': 3085,
|
|
||||||
'title': 'Bounce',
|
|
||||||
}, {
|
|
||||||
'start_time': 3085,
|
|
||||||
'end_time': 3232,
|
|
||||||
'title': 'Suggestions',
|
|
||||||
}, {
|
|
||||||
'start_time': 3232,
|
|
||||||
'end_time': 3493,
|
|
||||||
'title': 'Psycho',
|
|
||||||
}, {
|
|
||||||
'start_time': 3493,
|
|
||||||
'end_time': 3675,
|
|
||||||
'title': 'Chop Suey!',
|
|
||||||
}, {
|
|
||||||
'start_time': 3675,
|
|
||||||
'end_time': 3854,
|
|
||||||
'title': 'Lonely Day',
|
|
||||||
}, {
|
|
||||||
'start_time': 3854,
|
|
||||||
'end_time': 4090,
|
|
||||||
'title': 'Question!',
|
|
||||||
}, {
|
|
||||||
'start_time': 4090,
|
|
||||||
'end_time': 4420,
|
|
||||||
'title': 'Lost in Hollywood',
|
|
||||||
}, {
|
|
||||||
'start_time': 4420,
|
|
||||||
'end_time': 4577,
|
|
||||||
'title': 'Vicinity of Obscenity (First live performance since 2012)',
|
|
||||||
}, {
|
|
||||||
'start_time': 4577,
|
|
||||||
'end_time': 4802,
|
|
||||||
'title': 'Forest',
|
|
||||||
}, {
|
|
||||||
'start_time': 4802,
|
|
||||||
'end_time': 5037,
|
|
||||||
'title': 'Cigaro',
|
|
||||||
}, {
|
|
||||||
'start_time': 5037,
|
|
||||||
'end_time': 5273,
|
|
||||||
'title': 'Toxicity (with Chino Moreno)',
|
|
||||||
}, {
|
|
||||||
'start_time': 5273,
|
|
||||||
'end_time': 5640,
|
|
||||||
'title': 'Sugar',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=PkYLQbsqCE8
|
|
||||||
# pattern: <num> - <title> [<latinized title>] 0:00:00
|
|
||||||
'''Затемно (Zatemno) is an Obscure Black Metal Band from Russia.<br /><br />"Во прах (Vo prakh)'' Into The Ashes", Debut mini-album released may 6, 2016, by Death Knell Productions<br />Released on 6 panel digipak CD, limited to 100 copies only<br />And digital format on Bandcamp<br /><br />Tracklist<br /><br />1 - Во прах [Vo prakh] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+00*60+00);return false;">0:00:00</a><br />2 - Искупление [Iskupleniye] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+08*60+10);return false;">0:08:10</a><br />3 - Из серпов луны...[Iz serpov luny] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+14*60+30);return false;">0:14:30</a><br /><br />Links:<br /><a href="https://deathknellprod.bandcamp.com/album/--2" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://deathknellprod.bandcamp.com/album/--2" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://deathknellprod.bandcamp.com/a...</a><br /><a href="https://www.facebook.com/DeathKnellProd/" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://www.facebook.com/DeathKnellProd/" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://www.facebook.com/DeathKnellProd/</a><br /><br /><br />I don't have any right about this artifact, my only intention is to spread the music of the band, all rights are reserved to the Затемно (Zatemno) and his producers, Death Knell Productions.<br /><br />------------------------------------------------------------------<br /><br />Subscribe for more videos like this.<br />My link: <a href="https://web.facebook.com/AttackOfTheDragons" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://web.facebook.com/AttackOfTheDragons" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://web.facebook.com/AttackOfTheD...</a>''',
|
|
||||||
1138,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 490,
|
|
||||||
'title': '1 - Во прах [Vo prakh]',
|
|
||||||
}, {
|
|
||||||
'start_time': 490,
|
|
||||||
'end_time': 870,
|
|
||||||
'title': '2 - Искупление [Iskupleniye]',
|
|
||||||
}, {
|
|
||||||
'start_time': 870,
|
|
||||||
'end_time': 1138,
|
|
||||||
'title': '3 - Из серпов луны...[Iz serpov luny]',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=xZW70zEasOk
|
|
||||||
# time point more than duration
|
|
||||||
'''● LCS Spring finals: Saturday and Sunday from <a href="#" onclick="yt.www.watch.player.seekTo(13*60+30);return false;">13:30</a> outside the venue! <br />● PAX East: Fri, Sat & Sun - more info in tomorrows video on the main channel!''',
|
|
||||||
283,
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_youtube_chapters(self):
|
|
||||||
for description, duration, expected_chapters in self._TEST_CASES:
|
|
||||||
ie = YoutubeIE()
|
|
||||||
expect_value(
|
|
||||||
self, ie._extract_chapters_from_description(description, duration),
|
|
||||||
expected_chapters, None)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -86,13 +86,9 @@ class TestPlayerInfo(unittest.TestCase):
|
|||||||
('https://www.youtube.com/yts/jsbin/player-en_US-vflaxXRn1/base.js', 'vflaxXRn1'),
|
('https://www.youtube.com/yts/jsbin/player-en_US-vflaxXRn1/base.js', 'vflaxXRn1'),
|
||||||
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js', 'vflXGBaUN'),
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js', 'vflXGBaUN'),
|
||||||
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js', 'vflKjOTVq'),
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js', 'vflKjOTVq'),
|
||||||
('http://s.ytimg.com/yt/swfbin/watch_as3-vflrEm9Nq.swf', 'vflrEm9Nq'),
|
|
||||||
('https://s.ytimg.com/yts/swfbin/player-vflenCdZL/watch_as3.swf', 'vflenCdZL'),
|
|
||||||
)
|
)
|
||||||
for player_url, expected_player_id in PLAYER_URLS:
|
for player_url, expected_player_id in PLAYER_URLS:
|
||||||
expected_player_type = player_url.split('.')[-1]
|
player_id = YoutubeIE._extract_player_info(player_url)
|
||||||
player_type, player_id = YoutubeIE._extract_player_info(player_url)
|
|
||||||
self.assertEqual(player_type, expected_player_type)
|
|
||||||
self.assertEqual(player_id, expected_player_id)
|
self.assertEqual(player_id, expected_player_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+183
-140
@@ -49,6 +49,7 @@ from .utils import (
|
|||||||
date_from_str,
|
date_from_str,
|
||||||
DateRange,
|
DateRange,
|
||||||
DEFAULT_OUTTMPL,
|
DEFAULT_OUTTMPL,
|
||||||
|
OUTTMPL_TYPES,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
determine_protocol,
|
determine_protocol,
|
||||||
DOT_DESKTOP_LINK_TEMPLATE,
|
DOT_DESKTOP_LINK_TEMPLATE,
|
||||||
@@ -61,6 +62,7 @@ from .utils import (
|
|||||||
ExistingVideoReached,
|
ExistingVideoReached,
|
||||||
expand_path,
|
expand_path,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
format_field,
|
format_field,
|
||||||
formatSeconds,
|
formatSeconds,
|
||||||
@@ -91,6 +93,7 @@ from .utils import (
|
|||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
std_headers,
|
std_headers,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
strftime_or_none,
|
||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
to_high_limit_path,
|
to_high_limit_path,
|
||||||
UnavailableVideoError,
|
UnavailableVideoError,
|
||||||
@@ -172,15 +175,20 @@ class YoutubeDL(object):
|
|||||||
forcejson: Force printing info_dict as JSON.
|
forcejson: Force printing info_dict as JSON.
|
||||||
dump_single_json: Force printing the info_dict of the whole playlist
|
dump_single_json: Force printing the info_dict of the whole playlist
|
||||||
(or video) as a single JSON line.
|
(or video) as a single JSON line.
|
||||||
force_write_download_archive: Force writing download archive regardless of
|
force_write_download_archive: Force writing download archive regardless
|
||||||
'skip_download' or 'simulate'.
|
of 'skip_download' or 'simulate'.
|
||||||
simulate: Do not download the video files.
|
simulate: Do not download the video files.
|
||||||
format: Video format code. see "FORMAT SELECTION" for more details.
|
format: Video format code. see "FORMAT SELECTION" for more details.
|
||||||
format_sort: How to sort the video formats. see "Sorting Formats" for more details.
|
format_sort: How to sort the video formats. see "Sorting Formats"
|
||||||
format_sort_force: Force the given format_sort. see "Sorting Formats" for more details.
|
for more details.
|
||||||
allow_multiple_video_streams: Allow multiple video streams to be merged into a single file
|
format_sort_force: Force the given format_sort. see "Sorting Formats"
|
||||||
allow_multiple_audio_streams: Allow multiple audio streams to be merged into a single file
|
for more details.
|
||||||
outtmpl: Template for output names.
|
allow_multiple_video_streams: Allow multiple video streams to be merged
|
||||||
|
into a single file
|
||||||
|
allow_multiple_audio_streams: Allow multiple audio streams to be merged
|
||||||
|
into a single file
|
||||||
|
outtmpl: Dictionary of templates for output names. Allowed keys
|
||||||
|
are 'default' and the keys of OUTTMPL_TYPES (in utils.py)
|
||||||
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
||||||
restrictfilenames: Do not allow "&" and spaces in file names
|
restrictfilenames: Do not allow "&" and spaces in file names
|
||||||
trim_file_name: Limit length of filename (extension excluded)
|
trim_file_name: Limit length of filename (extension excluded)
|
||||||
@@ -206,6 +214,8 @@ class YoutubeDL(object):
|
|||||||
unless writeinfojson is also given
|
unless writeinfojson is also given
|
||||||
writeannotations: Write the video annotations to a .annotations.xml file
|
writeannotations: Write the video annotations to a .annotations.xml file
|
||||||
writethumbnail: Write the thumbnail image to a file
|
writethumbnail: Write the thumbnail image to a file
|
||||||
|
allow_playlist_files: Whether to write playlists' description, infojson etc
|
||||||
|
also to disk when using the 'write*' options
|
||||||
write_all_thumbnails: Write all thumbnail formats to files
|
write_all_thumbnails: Write all thumbnail formats to files
|
||||||
writelink: Write an internet shortcut file, depending on the
|
writelink: Write an internet shortcut file, depending on the
|
||||||
current platform (.url/.webloc/.desktop)
|
current platform (.url/.webloc/.desktop)
|
||||||
@@ -446,6 +456,9 @@ class YoutubeDL(object):
|
|||||||
self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given')
|
self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given')
|
||||||
self.params['merge_output_format'] = self.params['final_ext']
|
self.params['merge_output_format'] = self.params['final_ext']
|
||||||
|
|
||||||
|
if 'overwrites' in self.params and self.params['overwrites'] is None:
|
||||||
|
del self.params['overwrites']
|
||||||
|
|
||||||
check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
|
check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
|
||||||
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
|
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
|
||||||
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
|
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
|
||||||
@@ -487,10 +500,7 @@ class YoutubeDL(object):
|
|||||||
'Set the LC_ALL environment variable to fix this.')
|
'Set the LC_ALL environment variable to fix this.')
|
||||||
self.params['restrictfilenames'] = True
|
self.params['restrictfilenames'] = True
|
||||||
|
|
||||||
if isinstance(params.get('outtmpl'), bytes):
|
self.outtmpl_dict = self.parse_outtmpl()
|
||||||
self.report_warning(
|
|
||||||
'Parameter outtmpl is bytes, but should be a unicode string. '
|
|
||||||
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
|
|
||||||
|
|
||||||
self._setup_opener()
|
self._setup_opener()
|
||||||
|
|
||||||
@@ -722,15 +732,33 @@ class YoutubeDL(object):
|
|||||||
def report_file_delete(self, file_name):
|
def report_file_delete(self, file_name):
|
||||||
"""Report that existing file will be deleted."""
|
"""Report that existing file will be deleted."""
|
||||||
try:
|
try:
|
||||||
self.to_screen('Deleting already existent file %s' % file_name)
|
self.to_screen('Deleting existing file %s' % file_name)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
self.to_screen('Deleting already existent file')
|
self.to_screen('Deleting existing file')
|
||||||
|
|
||||||
def prepare_filename(self, info_dict, warn=False):
|
def parse_outtmpl(self):
|
||||||
"""Generate the output filename."""
|
outtmpl_dict = self.params.get('outtmpl', {})
|
||||||
|
if not isinstance(outtmpl_dict, dict):
|
||||||
|
outtmpl_dict = {'default': outtmpl_dict}
|
||||||
|
outtmpl_dict.update({
|
||||||
|
k: v for k, v in DEFAULT_OUTTMPL.items()
|
||||||
|
if not outtmpl_dict.get(k)})
|
||||||
|
for key, val in outtmpl_dict.items():
|
||||||
|
if isinstance(val, bytes):
|
||||||
|
self.report_warning(
|
||||||
|
'Parameter outtmpl is bytes, but should be a unicode string. '
|
||||||
|
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
|
||||||
|
return outtmpl_dict
|
||||||
|
|
||||||
|
def _prepare_filename(self, info_dict, tmpl_type='default'):
|
||||||
try:
|
try:
|
||||||
template_dict = dict(info_dict)
|
template_dict = dict(info_dict)
|
||||||
|
|
||||||
|
template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
|
||||||
|
formatSeconds(info_dict['duration'], '-')
|
||||||
|
if info_dict.get('duration', None) is not None
|
||||||
|
else None)
|
||||||
|
|
||||||
template_dict['epoch'] = int(time.time())
|
template_dict['epoch'] = int(time.time())
|
||||||
autonumber_size = self.params.get('autonumber_size')
|
autonumber_size = self.params.get('autonumber_size')
|
||||||
if autonumber_size is None:
|
if autonumber_size is None:
|
||||||
@@ -751,9 +779,11 @@ class YoutubeDL(object):
|
|||||||
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
||||||
for k, v in template_dict.items()
|
for k, v in template_dict.items()
|
||||||
if v is not None and not isinstance(v, (list, tuple, dict)))
|
if v is not None and not isinstance(v, (list, tuple, dict)))
|
||||||
template_dict = collections.defaultdict(lambda: self.params.get('outtmpl_na_placeholder', 'NA'), template_dict)
|
na = self.params.get('outtmpl_na_placeholder', 'NA')
|
||||||
|
template_dict = collections.defaultdict(lambda: na, template_dict)
|
||||||
|
|
||||||
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
|
||||||
|
force_ext = OUTTMPL_TYPES.get(tmpl_type)
|
||||||
|
|
||||||
# For fields playlist_index and autonumber convert all occurrences
|
# For fields playlist_index and autonumber convert all occurrences
|
||||||
# of %(field)s to %(field)0Nd for backward compatibility
|
# of %(field)s to %(field)0Nd for backward compatibility
|
||||||
@@ -769,27 +799,45 @@ class YoutubeDL(object):
|
|||||||
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
|
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
|
||||||
outtmpl)
|
outtmpl)
|
||||||
|
|
||||||
|
# As of [1] format syntax is:
|
||||||
|
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
|
||||||
|
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
|
||||||
|
FORMAT_RE = r'''(?x)
|
||||||
|
(?<!%)
|
||||||
|
%
|
||||||
|
\({0}\) # mapping key
|
||||||
|
(?:[#0\-+ ]+)? # conversion flags (optional)
|
||||||
|
(?:\d+)? # minimum field width (optional)
|
||||||
|
(?:\.\d+)? # precision (optional)
|
||||||
|
[hlL]? # length modifier (optional)
|
||||||
|
(?P<type>[diouxXeEfFgGcrs%]) # conversion type
|
||||||
|
'''
|
||||||
|
|
||||||
|
numeric_fields = list(self._NUMERIC_FIELDS)
|
||||||
|
|
||||||
|
# Format date
|
||||||
|
FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
|
||||||
|
for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
|
||||||
|
conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
|
||||||
|
if key in template_dict:
|
||||||
|
continue
|
||||||
|
value = strftime_or_none(template_dict.get(field), frmt, na)
|
||||||
|
if conv_type in 'crs': # string
|
||||||
|
value = sanitize(field, value)
|
||||||
|
else: # number
|
||||||
|
numeric_fields.append(key)
|
||||||
|
value = float_or_none(value, default=None)
|
||||||
|
if value is not None:
|
||||||
|
template_dict[key] = value
|
||||||
|
|
||||||
# Missing numeric fields used together with integer presentation types
|
# Missing numeric fields used together with integer presentation types
|
||||||
# in format specification will break the argument substitution since
|
# in format specification will break the argument substitution since
|
||||||
# string NA placeholder is returned for missing fields. We will patch
|
# string NA placeholder is returned for missing fields. We will patch
|
||||||
# output template for missing fields to meet string presentation type.
|
# output template for missing fields to meet string presentation type.
|
||||||
for numeric_field in self._NUMERIC_FIELDS:
|
for numeric_field in numeric_fields:
|
||||||
if numeric_field not in template_dict:
|
if numeric_field not in template_dict:
|
||||||
# As of [1] format syntax is:
|
|
||||||
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
|
|
||||||
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
|
|
||||||
FORMAT_RE = r'''(?x)
|
|
||||||
(?<!%)
|
|
||||||
%
|
|
||||||
\({0}\) # mapping key
|
|
||||||
(?:[#0\-+ ]+)? # conversion flags (optional)
|
|
||||||
(?:\d+)? # minimum field width (optional)
|
|
||||||
(?:\.\d+)? # precision (optional)
|
|
||||||
[hlL]? # length modifier (optional)
|
|
||||||
[diouxXeEfFgGcrs%] # conversion type
|
|
||||||
'''
|
|
||||||
outtmpl = re.sub(
|
outtmpl = re.sub(
|
||||||
FORMAT_RE.format(numeric_field),
|
FORMAT_RE.format(re.escape(numeric_field)),
|
||||||
r'%({0})s'.format(numeric_field), outtmpl)
|
r'%({0})s'.format(numeric_field), outtmpl)
|
||||||
|
|
||||||
# expand_path translates '%%' into '%' and '$$' into '$'
|
# expand_path translates '%%' into '%' and '$$' into '$'
|
||||||
@@ -805,6 +853,9 @@ class YoutubeDL(object):
|
|||||||
# title "Hello $PATH", we don't want `$PATH` to be expanded.
|
# title "Hello $PATH", we don't want `$PATH` to be expanded.
|
||||||
filename = expand_path(outtmpl).replace(sep, '') % template_dict
|
filename = expand_path(outtmpl).replace(sep, '') % template_dict
|
||||||
|
|
||||||
|
if force_ext is not None:
|
||||||
|
filename = replace_extension(filename, force_ext, template_dict.get('ext'))
|
||||||
|
|
||||||
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
||||||
trim_file_name = self.params.get('trim_file_name', False)
|
trim_file_name = self.params.get('trim_file_name', False)
|
||||||
if trim_file_name:
|
if trim_file_name:
|
||||||
@@ -822,25 +873,28 @@ class YoutubeDL(object):
|
|||||||
filename = encodeFilename(filename, True).decode(preferredencoding())
|
filename = encodeFilename(filename, True).decode(preferredencoding())
|
||||||
filename = sanitize_path(filename)
|
filename = sanitize_path(filename)
|
||||||
|
|
||||||
if warn and not self.__prepare_filename_warned:
|
|
||||||
if not self.params.get('paths'):
|
|
||||||
pass
|
|
||||||
elif filename == '-':
|
|
||||||
self.report_warning('--paths is ignored when an outputting to stdout')
|
|
||||||
elif os.path.isabs(filename):
|
|
||||||
self.report_warning('--paths is ignored since an absolute path is given in output template')
|
|
||||||
self.__prepare_filename_warned = True
|
|
||||||
|
|
||||||
return filename
|
return filename
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def prepare_filepath(self, filename, dir_type=''):
|
def prepare_filename(self, info_dict, dir_type='', warn=False):
|
||||||
if filename == '-':
|
"""Generate the output filename."""
|
||||||
return filename
|
|
||||||
paths = self.params.get('paths', {})
|
paths = self.params.get('paths', {})
|
||||||
assert isinstance(paths, dict)
|
assert isinstance(paths, dict)
|
||||||
|
filename = self._prepare_filename(info_dict, dir_type or 'default')
|
||||||
|
|
||||||
|
if warn and not self.__prepare_filename_warned:
|
||||||
|
if not paths:
|
||||||
|
pass
|
||||||
|
elif filename == '-':
|
||||||
|
self.report_warning('--paths is ignored when an outputting to stdout')
|
||||||
|
elif os.path.isabs(filename):
|
||||||
|
self.report_warning('--paths is ignored since an absolute path is given in output template')
|
||||||
|
self.__prepare_filename_warned = True
|
||||||
|
if filename == '-' or not filename:
|
||||||
|
return filename
|
||||||
|
|
||||||
homepath = expand_path(paths.get('home', '').strip())
|
homepath = expand_path(paths.get('home', '').strip())
|
||||||
assert isinstance(homepath, compat_str)
|
assert isinstance(homepath, compat_str)
|
||||||
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
|
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
|
||||||
@@ -992,10 +1046,6 @@ class YoutubeDL(object):
|
|||||||
self.add_extra_info(ie_result, {
|
self.add_extra_info(ie_result, {
|
||||||
'extractor': ie.IE_NAME,
|
'extractor': ie.IE_NAME,
|
||||||
'webpage_url': url,
|
'webpage_url': url,
|
||||||
'duration_string': (
|
|
||||||
formatSeconds(ie_result['duration'], '-')
|
|
||||||
if ie_result.get('duration', None) is not None
|
|
||||||
else None),
|
|
||||||
'webpage_url_basename': url_basename(url),
|
'webpage_url_basename': url_basename(url),
|
||||||
'extractor_key': ie.ie_key(),
|
'extractor_key': ie.ie_key(),
|
||||||
})
|
})
|
||||||
@@ -1015,10 +1065,7 @@ class YoutubeDL(object):
|
|||||||
extract_flat = self.params.get('extract_flat', False)
|
extract_flat = self.params.get('extract_flat', False)
|
||||||
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
|
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
|
||||||
or extract_flat is True):
|
or extract_flat is True):
|
||||||
self.__forced_printings(
|
self.__forced_printings(ie_result, self.prepare_filename(ie_result), incomplete=True)
|
||||||
ie_result,
|
|
||||||
self.prepare_filepath(self.prepare_filename(ie_result)),
|
|
||||||
incomplete=True)
|
|
||||||
return ie_result
|
return ie_result
|
||||||
|
|
||||||
if result_type == 'video':
|
if result_type == 'video':
|
||||||
@@ -1108,54 +1155,53 @@ class YoutubeDL(object):
|
|||||||
# We process each entry in the playlist
|
# We process each entry in the playlist
|
||||||
playlist = ie_result.get('title') or ie_result.get('id')
|
playlist = ie_result.get('title') or ie_result.get('id')
|
||||||
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
||||||
ie_copy = {
|
|
||||||
'playlist': playlist,
|
|
||||||
'playlist_id': ie_result.get('id'),
|
|
||||||
'playlist_title': ie_result.get('title'),
|
|
||||||
'playlist_uploader': ie_result.get('uploader'),
|
|
||||||
'playlist_uploader_id': ie_result.get('uploader_id'),
|
|
||||||
'playlist_index': 0
|
|
||||||
}
|
|
||||||
ie_copy.update(dict(ie_result))
|
|
||||||
|
|
||||||
def ensure_dir_exists(path):
|
if self.params.get('allow_playlist_files', True):
|
||||||
return make_dir(path, self.report_error)
|
ie_copy = {
|
||||||
|
'playlist': playlist,
|
||||||
|
'playlist_id': ie_result.get('id'),
|
||||||
|
'playlist_title': ie_result.get('title'),
|
||||||
|
'playlist_uploader': ie_result.get('uploader'),
|
||||||
|
'playlist_uploader_id': ie_result.get('uploader_id'),
|
||||||
|
'playlist_index': 0
|
||||||
|
}
|
||||||
|
ie_copy.update(dict(ie_result))
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
def ensure_dir_exists(path):
|
||||||
infofn = replace_extension(
|
return make_dir(path, self.report_error)
|
||||||
self.prepare_filepath(self.prepare_filename(ie_copy), 'infojson'),
|
|
||||||
'info.json', ie_result.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(infofn)):
|
|
||||||
return
|
|
||||||
if self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
|
||||||
self.to_screen('[info] Playlist metadata is already present')
|
|
||||||
else:
|
|
||||||
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
|
|
||||||
playlist_info = dict(ie_result)
|
|
||||||
playlist_info.pop('entries')
|
|
||||||
try:
|
|
||||||
write_json_file(self.filter_requested_info(playlist_info), infofn)
|
|
||||||
except (OSError, IOError):
|
|
||||||
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
|
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writeinfojson', False):
|
||||||
descfn = replace_extension(
|
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
|
||||||
self.prepare_filepath(self.prepare_filename(ie_copy), 'description'),
|
if not ensure_dir_exists(encodeFilename(infofn)):
|
||||||
'description', ie_result.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(descfn)):
|
|
||||||
return
|
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
|
||||||
self.to_screen('[info] Playlist description is already present')
|
|
||||||
elif ie_result.get('description') is None:
|
|
||||||
self.report_warning('There\'s no playlist description to write.')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.to_screen('[info] Writing playlist description to: ' + descfn)
|
|
||||||
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
|
||||||
descfile.write(ie_result['description'])
|
|
||||||
except (OSError, IOError):
|
|
||||||
self.report_error('Cannot write playlist description file ' + descfn)
|
|
||||||
return
|
return
|
||||||
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
|
self.to_screen('[info] Playlist metadata is already present')
|
||||||
|
else:
|
||||||
|
playlist_info = dict(ie_result)
|
||||||
|
# playlist_info['entries'] = list(playlist_info['entries']) # Entries is a generator which shouldnot be resolved here
|
||||||
|
del playlist_info['entries']
|
||||||
|
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
|
||||||
|
try:
|
||||||
|
write_json_file(self.filter_requested_info(playlist_info), infofn)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
|
||||||
|
|
||||||
|
if self.params.get('writedescription', False):
|
||||||
|
descfn = self.prepare_filename(ie_copy, 'pl_description')
|
||||||
|
if not ensure_dir_exists(encodeFilename(descfn)):
|
||||||
|
return
|
||||||
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
|
self.to_screen('[info] Playlist description is already present')
|
||||||
|
elif ie_result.get('description') is None:
|
||||||
|
self.report_warning('There\'s no playlist description to write.')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.to_screen('[info] Writing playlist description to: ' + descfn)
|
||||||
|
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||||
|
descfile.write(ie_result['description'])
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error('Cannot write playlist description file ' + descfn)
|
||||||
|
return
|
||||||
|
|
||||||
playlist_results = []
|
playlist_results = []
|
||||||
|
|
||||||
@@ -1341,7 +1387,7 @@ class YoutubeDL(object):
|
|||||||
and (
|
and (
|
||||||
not can_merge()
|
not can_merge()
|
||||||
or info_dict.get('is_live', False)
|
or info_dict.get('is_live', False)
|
||||||
or self.params.get('outtmpl', DEFAULT_OUTTMPL) == '-'))
|
or self.outtmpl_dict['default'] == '-'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
'best/bestvideo+bestaudio'
|
'best/bestvideo+bestaudio'
|
||||||
@@ -1852,7 +1898,7 @@ class YoutubeDL(object):
|
|||||||
if req_format is None:
|
if req_format is None:
|
||||||
req_format = self._default_format_spec(info_dict, download=download)
|
req_format = self._default_format_spec(info_dict, download=download)
|
||||||
if self.params.get('verbose'):
|
if self.params.get('verbose'):
|
||||||
self._write_string('[debug] Default format spec: %s\n' % req_format)
|
self.to_screen('[debug] Default format spec: %s' % req_format)
|
||||||
|
|
||||||
format_selector = self.build_format_selector(req_format)
|
format_selector = self.build_format_selector(req_format)
|
||||||
|
|
||||||
@@ -2003,10 +2049,10 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
info_dict = self.pre_process(info_dict)
|
info_dict = self.pre_process(info_dict)
|
||||||
|
|
||||||
filename = self.prepare_filename(info_dict, warn=True)
|
info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
|
||||||
info_dict['_filename'] = full_filename = self.prepare_filepath(filename)
|
temp_filename = self.prepare_filename(info_dict, 'temp')
|
||||||
temp_filename = self.prepare_filepath(filename, 'temp')
|
|
||||||
files_to_move = {}
|
files_to_move = {}
|
||||||
|
skip_dl = self.params.get('skip_download', False)
|
||||||
|
|
||||||
# Forced printings
|
# Forced printings
|
||||||
self.__forced_printings(info_dict, full_filename, incomplete=False)
|
self.__forced_printings(info_dict, full_filename, incomplete=False)
|
||||||
@@ -2018,7 +2064,7 @@ class YoutubeDL(object):
|
|||||||
# Do nothing else if in simulate mode
|
# Do nothing else if in simulate mode
|
||||||
return
|
return
|
||||||
|
|
||||||
if filename is None:
|
if full_filename is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def ensure_dir_exists(path):
|
def ensure_dir_exists(path):
|
||||||
@@ -2030,9 +2076,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
descfn = replace_extension(
|
descfn = self.prepare_filename(info_dict, 'description')
|
||||||
self.prepare_filepath(filename, 'description'),
|
|
||||||
'description', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(descfn)):
|
if not ensure_dir_exists(encodeFilename(descfn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
@@ -2049,9 +2093,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeannotations', False):
|
if self.params.get('writeannotations', False):
|
||||||
annofn = replace_extension(
|
annofn = self.prepare_filename(info_dict, 'annotation')
|
||||||
self.prepare_filepath(filename, 'annotation'),
|
|
||||||
'annotations.xml', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(annofn)):
|
if not ensure_dir_exists(encodeFilename(annofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
||||||
@@ -2087,10 +2129,11 @@ class YoutubeDL(object):
|
|||||||
# ie = self.get_info_extractor(info_dict['extractor_key'])
|
# ie = self.get_info_extractor(info_dict['extractor_key'])
|
||||||
for sub_lang, sub_info in subtitles.items():
|
for sub_lang, sub_info in subtitles.items():
|
||||||
sub_format = sub_info['ext']
|
sub_format = sub_info['ext']
|
||||||
sub_filename = subtitles_filename(temp_filename, sub_lang, sub_format, info_dict.get('ext'))
|
sub_fn = self.prepare_filename(info_dict, 'subtitle')
|
||||||
sub_filename_final = subtitles_filename(
|
sub_filename = subtitles_filename(
|
||||||
self.prepare_filepath(filename, 'subtitle'),
|
temp_filename if not skip_dl else sub_fn,
|
||||||
sub_lang, sub_format, info_dict.get('ext'))
|
sub_lang, sub_format, info_dict.get('ext'))
|
||||||
|
sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext'))
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
|
||||||
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
|
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
|
||||||
files_to_move[sub_filename] = sub_filename_final
|
files_to_move[sub_filename] = sub_filename_final
|
||||||
@@ -2124,10 +2167,10 @@ class YoutubeDL(object):
|
|||||||
(sub_lang, error_to_compat_str(err)))
|
(sub_lang, error_to_compat_str(err)))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.params.get('skip_download', False):
|
if skip_dl:
|
||||||
if self.params.get('convertsubtitles', False):
|
if self.params.get('convertsubtitles', False):
|
||||||
# subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
|
# subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
|
||||||
filename_real_ext = os.path.splitext(filename)[1][1:]
|
filename_real_ext = os.path.splitext(full_filename)[1][1:]
|
||||||
filename_wo_ext = (
|
filename_wo_ext = (
|
||||||
os.path.splitext(full_filename)[0]
|
os.path.splitext(full_filename)[0]
|
||||||
if filename_real_ext == info_dict['ext']
|
if filename_real_ext == info_dict['ext']
|
||||||
@@ -2142,14 +2185,12 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.post_process(full_filename, info_dict, files_to_move)
|
self.post_process(full_filename, info_dict, files_to_move)
|
||||||
except (PostProcessingError) as err:
|
except PostProcessingError as err:
|
||||||
self.report_error('postprocessing: %s' % str(err))
|
self.report_error('Postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = replace_extension(
|
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||||
self.prepare_filepath(filename, 'infojson'),
|
|
||||||
'info.json', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(infofn)):
|
if not ensure_dir_exists(encodeFilename(infofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
@@ -2161,11 +2202,14 @@ class YoutubeDL(object):
|
|||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.report_error('Cannot write video metadata to JSON file ' + infofn)
|
self.report_error('Cannot write video metadata to JSON file ' + infofn)
|
||||||
return
|
return
|
||||||
info_dict['__infojson_filepath'] = infofn
|
info_dict['__infojson_filename'] = infofn
|
||||||
|
|
||||||
thumbdir = os.path.dirname(self.prepare_filepath(filename, 'thumbnail'))
|
thumbfn = self.prepare_filename(info_dict, 'thumbnail')
|
||||||
for thumbfn in self._write_thumbnails(info_dict, temp_filename):
|
thumb_fn_temp = temp_filename if not skip_dl else thumbfn
|
||||||
files_to_move[thumbfn] = os.path.join(thumbdir, os.path.basename(thumbfn))
|
for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp):
|
||||||
|
thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext'))
|
||||||
|
thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext'))
|
||||||
|
files_to_move[thumb_filename_temp] = info_dict['__thumbnail_filename'] = thumb_filename
|
||||||
|
|
||||||
# Write internet shortcut files
|
# Write internet shortcut files
|
||||||
url_link = webloc_link = desktop_link = False
|
url_link = webloc_link = desktop_link = False
|
||||||
@@ -2218,7 +2262,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
# Download
|
# Download
|
||||||
must_record_download_archive = False
|
must_record_download_archive = False
|
||||||
if not self.params.get('skip_download', False):
|
if not skip_dl:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def existing_file(*filepaths):
|
def existing_file(*filepaths):
|
||||||
@@ -2298,7 +2342,7 @@ class YoutubeDL(object):
|
|||||||
new_info = dict(info_dict)
|
new_info = dict(info_dict)
|
||||||
new_info.update(f)
|
new_info.update(f)
|
||||||
fname = prepend_extension(
|
fname = prepend_extension(
|
||||||
self.prepare_filepath(self.prepare_filename(new_info), 'temp'),
|
self.prepare_filename(new_info, 'temp'),
|
||||||
'f%s' % f['format_id'], new_info['ext'])
|
'f%s' % f['format_id'], new_info['ext'])
|
||||||
if not ensure_dir_exists(fname):
|
if not ensure_dir_exists(fname):
|
||||||
return
|
return
|
||||||
@@ -2328,7 +2372,7 @@ class YoutubeDL(object):
|
|||||||
self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
||||||
return
|
return
|
||||||
|
|
||||||
if success and filename != '-':
|
if success and full_filename != '-':
|
||||||
# Fixup content
|
# Fixup content
|
||||||
fixup_policy = self.params.get('fixup')
|
fixup_policy = self.params.get('fixup')
|
||||||
if fixup_policy is None:
|
if fixup_policy is None:
|
||||||
@@ -2391,8 +2435,8 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.post_process(dl_filename, info_dict, files_to_move)
|
self.post_process(dl_filename, info_dict, files_to_move)
|
||||||
except (PostProcessingError) as err:
|
except PostProcessingError as err:
|
||||||
self.report_error('postprocessing: %s' % str(err))
|
self.report_error('Postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
for ph in self._post_hooks:
|
for ph in self._post_hooks:
|
||||||
@@ -2410,7 +2454,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
def download(self, url_list):
|
def download(self, url_list):
|
||||||
"""Download a given list of URLs."""
|
"""Download a given list of URLs."""
|
||||||
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
outtmpl = self.outtmpl_dict['default']
|
||||||
if (len(url_list) > 1
|
if (len(url_list) > 1
|
||||||
and outtmpl != '-'
|
and outtmpl != '-'
|
||||||
and '%' not in outtmpl
|
and '%' not in outtmpl
|
||||||
@@ -2458,16 +2502,14 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_requested_info(info_dict):
|
def filter_requested_info(info_dict):
|
||||||
|
fields_to_remove = ('requested_formats', 'requested_subtitles')
|
||||||
return dict(
|
return dict(
|
||||||
(k, v) for k, v in info_dict.items()
|
(k, v) for k, v in info_dict.items()
|
||||||
if k not in ['requested_formats', 'requested_subtitles'])
|
if (k[0] != '_' or k == '_type') and k not in fields_to_remove)
|
||||||
|
|
||||||
def run_pp(self, pp, infodict, files_to_move={}):
|
def run_pp(self, pp, infodict, files_to_move={}):
|
||||||
files_to_delete = []
|
files_to_delete = []
|
||||||
try:
|
files_to_delete, infodict = pp.run(infodict)
|
||||||
files_to_delete, infodict = pp.run(infodict)
|
|
||||||
except PostProcessingError as e:
|
|
||||||
self.report_error(e.msg)
|
|
||||||
if not files_to_delete:
|
if not files_to_delete:
|
||||||
return files_to_move, infodict
|
return files_to_move, infodict
|
||||||
|
|
||||||
@@ -2495,12 +2537,13 @@ class YoutubeDL(object):
|
|||||||
"""Run all the postprocessors on the given file."""
|
"""Run all the postprocessors on the given file."""
|
||||||
info = dict(ie_info)
|
info = dict(ie_info)
|
||||||
info['filepath'] = filename
|
info['filepath'] = filename
|
||||||
|
info['__files_to_move'] = {}
|
||||||
|
|
||||||
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
|
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
|
||||||
files_to_move, info = self.run_pp(pp, info, files_to_move)
|
files_to_move, info = self.run_pp(pp, info, files_to_move)
|
||||||
info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info, files_to_move)[1]
|
info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info)[1]
|
||||||
for pp in self._pps['aftermove']:
|
for pp in self._pps['aftermove']:
|
||||||
files_to_move, info = self.run_pp(pp, info, {})
|
info = self.run_pp(pp, info, {})[1]
|
||||||
|
|
||||||
def _make_archive_id(self, info_dict):
|
def _make_archive_id(self, info_dict):
|
||||||
video_id = info_dict.get('id')
|
video_id = info_dict.get('id')
|
||||||
@@ -2851,7 +2894,7 @@ class YoutubeDL(object):
|
|||||||
encoding = preferredencoding()
|
encoding = preferredencoding()
|
||||||
return encoding
|
return encoding
|
||||||
|
|
||||||
def _write_thumbnails(self, info_dict, filename):
|
def _write_thumbnails(self, info_dict, filename): # return the extensions
|
||||||
if self.params.get('writethumbnail', False):
|
if self.params.get('writethumbnail', False):
|
||||||
thumbnails = info_dict.get('thumbnails')
|
thumbnails = info_dict.get('thumbnails')
|
||||||
if thumbnails:
|
if thumbnails:
|
||||||
@@ -2864,12 +2907,12 @@ class YoutubeDL(object):
|
|||||||
ret = []
|
ret = []
|
||||||
for t in thumbnails:
|
for t in thumbnails:
|
||||||
thumb_ext = determine_ext(t['url'], 'jpg')
|
thumb_ext = determine_ext(t['url'], 'jpg')
|
||||||
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
suffix = '%s.' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
||||||
t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext'))
|
t['filename'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
|
||||||
|
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
|
||||||
ret.append(thumb_filename)
|
ret.append(suffix + thumb_ext)
|
||||||
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
||||||
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
||||||
else:
|
else:
|
||||||
@@ -2879,7 +2922,7 @@ class YoutubeDL(object):
|
|||||||
uf = self.urlopen(t['url'])
|
uf = self.urlopen(t['url'])
|
||||||
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
||||||
shutil.copyfileobj(uf, thumbf)
|
shutil.copyfileobj(uf, thumbf)
|
||||||
ret.append(thumb_filename)
|
ret.append(suffix + thumb_ext)
|
||||||
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
||||||
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
||||||
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
||||||
|
|||||||
+14
-11
@@ -237,18 +237,21 @@ def _real_main(argv=None):
|
|||||||
if opts.allsubtitles and not opts.writeautomaticsub:
|
if opts.allsubtitles and not opts.writeautomaticsub:
|
||||||
opts.writesubtitles = True
|
opts.writesubtitles = True
|
||||||
|
|
||||||
outtmpl = ((opts.outtmpl is not None and opts.outtmpl)
|
outtmpl = opts.outtmpl
|
||||||
or (opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s')
|
if not outtmpl:
|
||||||
or (opts.format == '-1' and '%(id)s-%(format)s.%(ext)s')
|
outtmpl = {'default': (
|
||||||
or (opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s')
|
'%(title)s-%(id)s-%(format)s.%(ext)s' if opts.format == '-1' and opts.usetitle
|
||||||
or (opts.usetitle and '%(title)s-%(id)s.%(ext)s')
|
else '%(id)s-%(format)s.%(ext)s' if opts.format == '-1'
|
||||||
or (opts.useid and '%(id)s.%(ext)s')
|
else '%(autonumber)s-%(title)s-%(id)s.%(ext)s' if opts.usetitle and opts.autonumber
|
||||||
or (opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s')
|
else '%(title)s-%(id)s.%(ext)s' if opts.usetitle
|
||||||
or DEFAULT_OUTTMPL)
|
else '%(id)s.%(ext)s' if opts.useid
|
||||||
if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
|
else '%(autonumber)s-%(id)s.%(ext)s' if opts.autonumber
|
||||||
|
else None)}
|
||||||
|
outtmpl_default = outtmpl.get('default')
|
||||||
|
if outtmpl_default is not None and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio:
|
||||||
parser.error('Cannot download a video and extract audio into the same'
|
parser.error('Cannot download a video and extract audio into the same'
|
||||||
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
||||||
' template'.format(outtmpl))
|
' template'.format(outtmpl_default))
|
||||||
|
|
||||||
for f in opts.format_sort:
|
for f in opts.format_sort:
|
||||||
if re.match(InfoExtractor.FormatSort.regex, f) is None:
|
if re.match(InfoExtractor.FormatSort.regex, f) is None:
|
||||||
@@ -413,7 +416,7 @@ def _real_main(argv=None):
|
|||||||
'playlistreverse': opts.playlist_reverse,
|
'playlistreverse': opts.playlist_reverse,
|
||||||
'playlistrandom': opts.playlist_random,
|
'playlistrandom': opts.playlist_random,
|
||||||
'noplaylist': opts.noplaylist,
|
'noplaylist': opts.noplaylist,
|
||||||
'logtostderr': opts.outtmpl == '-',
|
'logtostderr': outtmpl_default == '-',
|
||||||
'consoletitle': opts.consoletitle,
|
'consoletitle': opts.consoletitle,
|
||||||
'nopart': opts.nopart,
|
'nopart': opts.nopart,
|
||||||
'updatetime': opts.updatetime,
|
'updatetime': opts.updatetime,
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class FileDownloader(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
nooverwrites_and_exists = (
|
nooverwrites_and_exists = (
|
||||||
not self.params.get('overwrites', True)
|
not self.params.get('overwrites', subtitle)
|
||||||
and os.path.exists(encodeFilename(filename))
|
and os.path.exists(encodeFilename(filename))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
ffpp = FFmpegPostProcessor(downloader=self)
|
ffpp = FFmpegPostProcessor(downloader=self)
|
||||||
if not ffpp.available:
|
if not ffpp.available:
|
||||||
self.report_error('m3u8 download detected but ffmpeg could not be found. Please install one.')
|
self.report_error('m3u8 download detected but ffmpeg could not be found. Please install')
|
||||||
return False
|
return False
|
||||||
ffpp.check_version()
|
ffpp.check_version()
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import re
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
|
from ..compat import compat_urllib_error
|
||||||
|
from ..utils import try_get
|
||||||
|
from ..extractor.youtube import YoutubeBaseInfoExtractor as YT_BaseIE
|
||||||
|
|
||||||
|
|
||||||
class YoutubeLiveChatReplayFD(FragmentFD):
|
class YoutubeLiveChatReplayFD(FragmentFD):
|
||||||
@@ -15,6 +18,7 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
video_id = info_dict['video_id']
|
video_id = info_dict['video_id']
|
||||||
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
||||||
|
|
||||||
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
test = self.params.get('test', False)
|
test = self.params.get('test', False)
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
@@ -28,15 +32,52 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
return self._download_fragment(ctx, url, info_dict, headers)
|
return self._download_fragment(ctx, url, info_dict, headers)
|
||||||
|
|
||||||
def parse_yt_initial_data(data):
|
def parse_yt_initial_data(data):
|
||||||
window_patt = b'window\\["ytInitialData"\\]\\s*=\\s*(.*?)(?<=});'
|
patterns = (
|
||||||
var_patt = b'var\\s+ytInitialData\\s*=\\s*(.*?)(?<=});'
|
r'%s\\s*%s' % (YT_BaseIE._YT_INITIAL_DATA_RE, YT_BaseIE._YT_INITIAL_BOUNDARY_RE),
|
||||||
for patt in window_patt, var_patt:
|
r'%s' % YT_BaseIE._YT_INITIAL_DATA_RE)
|
||||||
|
data = data.decode('utf-8', 'replace')
|
||||||
|
for patt in patterns:
|
||||||
try:
|
try:
|
||||||
raw_json = re.search(patt, data).group(1)
|
raw_json = re.search(patt, data).group(1)
|
||||||
return json.loads(raw_json)
|
return json.loads(raw_json)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def download_and_parse_fragment(url, frag_index):
|
||||||
|
count = 0
|
||||||
|
while count <= fragment_retries:
|
||||||
|
try:
|
||||||
|
success, raw_fragment = dl_fragment(url)
|
||||||
|
if not success:
|
||||||
|
return False, None, None
|
||||||
|
data = parse_yt_initial_data(raw_fragment) or json.loads(raw_fragment)['response']
|
||||||
|
|
||||||
|
live_chat_continuation = try_get(
|
||||||
|
data,
|
||||||
|
lambda x: x['continuationContents']['liveChatContinuation'], dict) or {}
|
||||||
|
offset = continuation_id = None
|
||||||
|
processed_fragment = bytearray()
|
||||||
|
for action in live_chat_continuation.get('actions', []):
|
||||||
|
if 'replayChatItemAction' in action:
|
||||||
|
replay_chat_item_action = action['replayChatItemAction']
|
||||||
|
offset = int(replay_chat_item_action['videoOffsetTimeMsec'])
|
||||||
|
processed_fragment.extend(
|
||||||
|
json.dumps(action, ensure_ascii=False).encode('utf-8') + b'\n')
|
||||||
|
if offset is not None:
|
||||||
|
continuation_id = try_get(
|
||||||
|
live_chat_continuation,
|
||||||
|
lambda x: x['continuations'][0]['liveChatReplayContinuationData']['continuation'])
|
||||||
|
self._append_fragment(ctx, processed_fragment)
|
||||||
|
|
||||||
|
return True, continuation_id, offset
|
||||||
|
except compat_urllib_error.HTTPError as err:
|
||||||
|
count += 1
|
||||||
|
if count <= fragment_retries:
|
||||||
|
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||||
|
if count > fragment_retries:
|
||||||
|
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
success, raw_fragment = dl_fragment(
|
success, raw_fragment = dl_fragment(
|
||||||
@@ -44,54 +85,25 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
data = parse_yt_initial_data(raw_fragment)
|
data = parse_yt_initial_data(raw_fragment)
|
||||||
continuation_id = data['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']
|
continuation_id = try_get(
|
||||||
|
data,
|
||||||
|
lambda x: x['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation'])
|
||||||
# no data yet but required to call _append_fragment
|
# no data yet but required to call _append_fragment
|
||||||
self._append_fragment(ctx, b'')
|
self._append_fragment(ctx, b'')
|
||||||
|
|
||||||
first = True
|
frag_index = offset = 0
|
||||||
offset = None
|
|
||||||
while continuation_id is not None:
|
while continuation_id is not None:
|
||||||
data = None
|
frag_index += 1
|
||||||
if first:
|
url = ''.join((
|
||||||
url = 'https://www.youtube.com/live_chat_replay?continuation={}'.format(continuation_id)
|
'https://www.youtube.com/live_chat_replay',
|
||||||
success, raw_fragment = dl_fragment(url)
|
'/get_live_chat_replay' if frag_index > 1 else '',
|
||||||
if not success:
|
'?continuation=%s' % continuation_id,
|
||||||
return False
|
'&playerOffsetMs=%d&hidden=false&pbj=1' % max(offset - 5000, 0) if frag_index > 1 else ''))
|
||||||
data = parse_yt_initial_data(raw_fragment)
|
success, continuation_id, offset = download_and_parse_fragment(url, frag_index)
|
||||||
else:
|
if not success:
|
||||||
url = ('https://www.youtube.com/live_chat_replay/get_live_chat_replay'
|
return False
|
||||||
+ '?continuation={}'.format(continuation_id)
|
if test:
|
||||||
+ '&playerOffsetMs={}'.format(max(offset - 5000, 0))
|
|
||||||
+ '&hidden=false'
|
|
||||||
+ '&pbj=1')
|
|
||||||
success, raw_fragment = dl_fragment(url)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
data = json.loads(raw_fragment)['response']
|
|
||||||
|
|
||||||
first = False
|
|
||||||
continuation_id = None
|
|
||||||
|
|
||||||
live_chat_continuation = data['continuationContents']['liveChatContinuation']
|
|
||||||
offset = None
|
|
||||||
processed_fragment = bytearray()
|
|
||||||
if 'actions' in live_chat_continuation:
|
|
||||||
for action in live_chat_continuation['actions']:
|
|
||||||
if 'replayChatItemAction' in action:
|
|
||||||
replay_chat_item_action = action['replayChatItemAction']
|
|
||||||
offset = int(replay_chat_item_action['videoOffsetTimeMsec'])
|
|
||||||
processed_fragment.extend(
|
|
||||||
json.dumps(action, ensure_ascii=False).encode('utf-8') + b'\n')
|
|
||||||
try:
|
|
||||||
continuation_id = live_chat_continuation['continuations'][0]['liveChatReplayContinuationData']['continuation']
|
|
||||||
except KeyError:
|
|
||||||
continuation_id = None
|
|
||||||
|
|
||||||
self._append_fragment(ctx, processed_fragment)
|
|
||||||
|
|
||||||
if test or offset is None:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import calendar
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from .amp import AMPIE
|
from .amp import AMPIE
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from ..utils import (
|
||||||
from ..compat import compat_urlparse
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbcNewsVideoIE(AMPIE):
|
class AbcNewsVideoIE(AMPIE):
|
||||||
@@ -18,8 +19,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
(?:
|
(?:
|
||||||
abcnews\.go\.com/
|
abcnews\.go\.com/
|
||||||
(?:
|
(?:
|
||||||
[^/]+/video/(?P<display_id>[0-9a-z-]+)-|
|
(?:[^/]+/)*video/(?P<display_id>[0-9a-z-]+)-|
|
||||||
video/embed\?.*?\bid=
|
video/(?:embed|itemfeed)\?.*?\bid=
|
||||||
)|
|
)|
|
||||||
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
||||||
'duration': 180,
|
'duration': 180,
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'timestamp': 1380454200,
|
||||||
|
'upload_date': '20130929',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -47,6 +50,12 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://abcnews.go.com/video/itemfeed?id=46979033',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://abcnews.go.com/GMA/News/video/history-christmas-story-67894761',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -67,28 +76,23 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://abcnews.go.com/Blotter/News/dramatic-video-rare-death-job-america/story?id=10498713#.UIhwosWHLjY',
|
# Youtube Embeds
|
||||||
|
'url': 'https://abcnews.go.com/Entertainment/peter-billingsley-child-actor-christmas-story-hollywood-power/story?id=51286501',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10505354',
|
'id': '51286501',
|
||||||
'ext': 'flv',
|
'title': "Peter Billingsley: From child actor in 'A Christmas Story' to Hollywood power player",
|
||||||
'display_id': 'dramatic-video-rare-death-job-america',
|
'description': 'Billingsley went from a child actor to Hollywood power player.',
|
||||||
'title': 'Occupational Hazards',
|
|
||||||
'description': 'Nightline investigates the dangers that lurk at various jobs.',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'upload_date': '20100428',
|
|
||||||
'timestamp': 1272412800,
|
|
||||||
},
|
},
|
||||||
'add_ie': ['AbcNewsVideo'],
|
'playlist_count': 5,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '38897857',
|
'id': '38897857',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'display_id': 'justin-timberlake-performs-stop-feeling-eurovision-2016',
|
|
||||||
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
||||||
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
||||||
'upload_date': '20160515',
|
'upload_date': '20160505',
|
||||||
'timestamp': 1463329500,
|
'timestamp': 1462442280,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -100,49 +104,55 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# inline.type == 'video'
|
||||||
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
story_id = self._match_id(url)
|
||||||
display_id = mobj.group('display_id')
|
webpage = self._download_webpage(url, story_id)
|
||||||
video_id = mobj.group('id')
|
story = self._parse_json(self._search_regex(
|
||||||
|
r"window\['__abcnews__'\]\s*=\s*({.+?});",
|
||||||
|
webpage, 'data'), story_id)['page']['content']['story']['everscroll'][0]
|
||||||
|
article_contents = story.get('articleContents') or {}
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
def entries():
|
||||||
video_url = self._search_regex(
|
featured_video = story.get('featuredVideo') or {}
|
||||||
r'window\.abcnvideo\.url\s*=\s*"([^"]+)"', webpage, 'video URL')
|
feed = try_get(featured_video, lambda x: x['video']['feed'])
|
||||||
full_video_url = compat_urlparse.urljoin(url, video_url)
|
if feed:
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': featured_video.get('id'),
|
||||||
|
'title': featured_video.get('name'),
|
||||||
|
'url': feed,
|
||||||
|
'thumbnail': featured_video.get('images'),
|
||||||
|
'description': featured_video.get('description'),
|
||||||
|
'timestamp': parse_iso8601(featured_video.get('uploadDate')),
|
||||||
|
'duration': parse_duration(featured_video.get('duration')),
|
||||||
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
youtube_url = YoutubeIE._extract_url(webpage)
|
for inline in (article_contents.get('inlines') or []):
|
||||||
|
inline_type = inline.get('type')
|
||||||
|
if inline_type == 'iframe':
|
||||||
|
iframe_url = try_get(inline, lambda x: x['attrs']['src'])
|
||||||
|
if iframe_url:
|
||||||
|
yield self.url_result(iframe_url)
|
||||||
|
elif inline_type == 'video':
|
||||||
|
video_id = inline.get('id')
|
||||||
|
if video_id:
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': video_id,
|
||||||
|
'url': 'http://abcnews.go.com/video/embed?id=' + video_id,
|
||||||
|
'thumbnail': inline.get('imgSrc') or inline.get('imgDefault'),
|
||||||
|
'description': inline.get('description'),
|
||||||
|
'duration': parse_duration(inline.get('duration')),
|
||||||
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
timestamp = None
|
return self.playlist_result(
|
||||||
date_str = self._html_search_regex(
|
entries(), story_id, article_contents.get('headline'),
|
||||||
r'<span[^>]+class="timestamp">([^<]+)</span>',
|
article_contents.get('subHead'))
|
||||||
webpage, 'timestamp', fatal=False)
|
|
||||||
if date_str:
|
|
||||||
tz_offset = 0
|
|
||||||
if date_str.endswith(' ET'): # Eastern Time
|
|
||||||
tz_offset = -5
|
|
||||||
date_str = date_str[:-3]
|
|
||||||
date_formats = ['%b. %d, %Y', '%b %d, %Y, %I:%M %p']
|
|
||||||
for date_format in date_formats:
|
|
||||||
try:
|
|
||||||
timestamp = calendar.timegm(time.strptime(date_str.strip(), date_format))
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if timestamp is not None:
|
|
||||||
timestamp -= tz_offset * 3600
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'ie_key': AbcNewsVideoIE.ie_key(),
|
|
||||||
'url': full_video_url,
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
if youtube_url:
|
|
||||||
entries = [entry, self.url_result(youtube_url, ie=YoutubeIE.ie_key())]
|
|
||||||
return self.playlist_result(entries)
|
|
||||||
|
|
||||||
return entry
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from ..utils import (
|
|||||||
strip_or_none,
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -51,9 +52,12 @@ class ADNIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_NETRC_MACHINE = 'animedigitalnetwork'
|
||||||
_BASE_URL = 'http://animedigitalnetwork.fr'
|
_BASE_URL = 'http://animedigitalnetwork.fr'
|
||||||
_API_BASE_URL = 'https://gw.api.animedigitalnetwork.fr/'
|
_API_BASE_URL = 'https://gw.api.animedigitalnetwork.fr/'
|
||||||
_PLAYER_BASE_URL = _API_BASE_URL + 'player/'
|
_PLAYER_BASE_URL = _API_BASE_URL + 'player/'
|
||||||
|
_HEADERS = {}
|
||||||
|
_LOGIN_ERR_MESSAGE = 'Unable to log in'
|
||||||
_RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
|
_RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
|
||||||
_POS_ALIGN_MAP = {
|
_POS_ALIGN_MAP = {
|
||||||
'start': 1,
|
'start': 1,
|
||||||
@@ -129,19 +133,42 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
}])
|
}])
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not username:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
access_token = (self._download_json(
|
||||||
|
self._API_BASE_URL + 'authentication/login', None,
|
||||||
|
'Logging in', self._LOGIN_ERR_MESSAGE, fatal=False,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'password': password,
|
||||||
|
'rememberMe': False,
|
||||||
|
'source': 'Web',
|
||||||
|
'username': username,
|
||||||
|
})) or {}).get('accessToken')
|
||||||
|
if access_token:
|
||||||
|
self._HEADERS = {'authorization': 'Bearer ' + access_token}
|
||||||
|
except ExtractorError as e:
|
||||||
|
message = None
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
resp = self._parse_json(
|
||||||
|
e.cause.read().decode(), None, fatal=False) or {}
|
||||||
|
message = resp.get('message') or resp.get('code')
|
||||||
|
self.report_warning(message or self._LOGIN_ERR_MESSAGE)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
|
video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
|
||||||
player = self._download_json(
|
player = self._download_json(
|
||||||
video_base_url + 'configuration', video_id,
|
video_base_url + 'configuration', video_id,
|
||||||
'Downloading player config JSON metadata')['player']
|
'Downloading player config JSON metadata',
|
||||||
|
headers=self._HEADERS)['player']
|
||||||
options = player['options']
|
options = player['options']
|
||||||
|
|
||||||
user = options['user']
|
user = options['user']
|
||||||
if not user.get('hasAccess'):
|
if not user.get('hasAccess'):
|
||||||
raise ExtractorError(
|
self.raise_login_required()
|
||||||
'This video is only available for paying users', expected=True)
|
|
||||||
# self.raise_login_required() # FIXME: Login is not implemented
|
|
||||||
|
|
||||||
token = self._download_json(
|
token = self._download_json(
|
||||||
user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
|
user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
|
||||||
@@ -188,8 +215,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
message = error.get('message')
|
message = error.get('message')
|
||||||
if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
|
if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
|
||||||
self.raise_geo_restricted(msg=message)
|
self.raise_geo_restricted(msg=message)
|
||||||
else:
|
raise ExtractorError(message)
|
||||||
raise ExtractorError(message)
|
|
||||||
else:
|
else:
|
||||||
raise ExtractorError('Giving up retrying')
|
raise ExtractorError('Giving up retrying')
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class AENetworksShowIE(AENetworksListBaseIE):
|
|||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.history.com/shows/ancient-aliens',
|
'url': 'http://www.history.com/shows/ancient-aliens',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'SH012427480000',
|
'id': 'SERIES1574',
|
||||||
'title': 'Ancient Aliens',
|
'title': 'Ancient Aliens',
|
||||||
'description': 'md5:3f6d74daf2672ff3ae29ed732e37ea7f',
|
'description': 'md5:3f6d74daf2672ff3ae29ed732e37ea7f',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class AMPIE(InfoExtractor):
|
|||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
timestamp = parse_iso8601(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
timestamp = unified_timestamp(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class AudiusBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
class AudiusIE(AudiusBaseIE):
|
class AudiusIE(AudiusBaseIE):
|
||||||
_VALID_URL = r'''(?x)https?://(?:www\.)?(?:audius\.co/(?P<uploader>[\w\d-]+)(?!/album|/playlist)/(?P<title>\S+))'''
|
_VALID_URL = r'''(?x)https?://(?:www\.)?(?:audius\.co/(?P<uploader>[\w\d-]+)(?!/album|/playlist)/(?P<title>\S+))'''
|
||||||
|
IE_DESC = 'Audius.co'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
# URL from Chrome address bar which replace backslash to forward slash
|
# URL from Chrome address bar which replace backslash to forward slash
|
||||||
@@ -124,11 +125,15 @@ class AudiusIE(AudiusBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
uploader, title, track_id = mobj.groups()
|
track_id = try_get(mobj, lambda x: x.group('track_id'))
|
||||||
if track_id is None:
|
if track_id is None:
|
||||||
|
title = mobj.group('title')
|
||||||
|
# uploader = mobj.group('uploader')
|
||||||
url = self._prepare_url(url, title)
|
url = self._prepare_url(url, title)
|
||||||
track_data = self._resolve_url(url, title)
|
track_data = self._resolve_url(url, title)
|
||||||
else: # API link
|
else: # API link
|
||||||
|
title = None
|
||||||
|
# uploader = None
|
||||||
track_data = self._api_request('/tracks/%s' % track_id, track_id)
|
track_data = self._api_request('/tracks/%s' % track_id, track_id)
|
||||||
|
|
||||||
if not isinstance(track_data, dict):
|
if not isinstance(track_data, dict):
|
||||||
@@ -167,9 +172,26 @@ class AudiusIE(AudiusBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AudiusTrackIE(AudiusIE):
|
||||||
|
_VALID_URL = r'''(?x)(?:audius:)(?:https?://(?:www\.)?.+/v1/tracks/)?(?P<track_id>\w+)'''
|
||||||
|
IE_NAME = 'audius:track'
|
||||||
|
IE_DESC = 'Audius track ID or API link. Prepend with "audius:"'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'audius:9RWlo',
|
||||||
|
'only_matching': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'audius:http://discoveryprovider.audius.prod-us-west-2.staked.cloud/v1/tracks/9RWlo',
|
||||||
|
'only_matching': True
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AudiusPlaylistIE(AudiusBaseIE):
|
class AudiusPlaylistIE(AudiusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?audius\.co/(?P<uploader>[\w\d-]+)/(?:album|playlist)/(?P<title>\S+)'
|
_VALID_URL = r'https?://(?:www\.)?audius\.co/(?P<uploader>[\w\d-]+)/(?:album|playlist)/(?P<title>\S+)'
|
||||||
IE_NAME = 'audius:playlist'
|
IE_NAME = 'audius:playlist'
|
||||||
|
IE_DESC = 'Audius.co playlists'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'https://audius.co/test_acc/playlist/test-playlist-22910',
|
'url': 'https://audius.co/test_acc/playlist/test-playlist-22910',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -189,14 +211,15 @@ class AudiusPlaylistIE(AudiusBaseIE):
|
|||||||
if not track_id:
|
if not track_id:
|
||||||
raise ExtractorError('Unable to get track ID from playlist')
|
raise ExtractorError('Unable to get track ID from playlist')
|
||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
'%s%s/tracks/%s' % (self._API_BASE, self._API_V, track_id),
|
'audius:%s' % track_id,
|
||||||
ie=AudiusIE.ie_key(), video_id=track_id))
|
ie=AudiusTrackIE.ie_key(), video_id=track_id))
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
self._select_api_base()
|
self._select_api_base()
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
uploader, title = mobj.groups()
|
title = mobj.group('title')
|
||||||
|
# uploader = mobj.group('uploader')
|
||||||
url = self._prepare_url(url, title)
|
url = self._prepare_url(url, title)
|
||||||
playlist_response = self._resolve_url(url, title)
|
playlist_response = self._resolve_url(url, title)
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class AWAANBaseIE(InfoExtractor):
|
|||||||
'duration': int_or_none(video_data.get('duration')),
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
'uploader_id': video_data.get('user_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ class AWAANLiveIE(AWAANBaseIE):
|
|||||||
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
'upload_date': '20150107',
|
'upload_date': '20150107',
|
||||||
'timestamp': 1420588800,
|
'timestamp': 1420588800,
|
||||||
|
'uploader_id': '71',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/cb9f2f81ed22e9b47f4ca64ea3cc5a5d13e88d1d'
|
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/a4016f65fe62b81dc6664dd9f4910e4ab40383be'
|
||||||
_PARTNER_ID = '1719221'
|
_PARTNER_ID = '1719221'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -90,13 +90,19 @@ class BleacherReportCMSIE(AMPIE):
|
|||||||
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
||||||
'md5': '2e4b0a997f9228ffa31fada5c53d1ed1',
|
'md5': '670b2d73f48549da032861130488c681',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
||||||
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
||||||
|
'upload_date': '20150723',
|
||||||
|
'timestamp': 1437679032,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
'Unable to download f4m manifest'
|
||||||
|
]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class BravoTVIE(AdobePassIE):
|
class BravoTVIE(AdobePassIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bravotv\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?P<req_id>bravotv|oxygen)\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
||||||
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
||||||
@@ -28,10 +28,13 @@ class BravoTVIE(AdobePassIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.oxygen.com/in-ice-cold-blood/season-2/episode-16/videos/handling-the-horwitz-house-after-the-murder-season-2',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
site, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
settings = self._parse_json(self._search_regex(
|
settings = self._parse_json(self._search_regex(
|
||||||
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
||||||
@@ -53,11 +56,14 @@ class BravoTVIE(AdobePassIE):
|
|||||||
tp_path = release_pid = tve['release_pid']
|
tp_path = release_pid = tve['release_pid']
|
||||||
if tve.get('entitlement') == 'auth':
|
if tve.get('entitlement') == 'auth':
|
||||||
adobe_pass = settings.get('tve_adobe_auth', {})
|
adobe_pass = settings.get('tve_adobe_auth', {})
|
||||||
|
if site == 'bravotv':
|
||||||
|
site = 'bravo'
|
||||||
resource = self._get_mvpd_resource(
|
resource = self._get_mvpd_resource(
|
||||||
adobe_pass.get('adobePassResourceId', 'bravo'),
|
adobe_pass.get('adobePassResourceId') or site,
|
||||||
tve['title'], release_pid, tve.get('rating'))
|
tve['title'], release_pid, tve.get('rating'))
|
||||||
query['auth'] = self._extract_mvpd_auth(
|
query['auth'] = self._extract_mvpd_auth(
|
||||||
url, release_pid, adobe_pass.get('adobePassRequestorId', 'bravo'), resource)
|
url, release_pid,
|
||||||
|
adobe_pass.get('adobePassRequestorId') or site, resource)
|
||||||
else:
|
else:
|
||||||
shared_playlist = settings['ls_playlist']
|
shared_playlist = settings['ls_playlist']
|
||||||
account_pid = shared_playlist['account_pid']
|
account_pid = shared_playlist['account_pid']
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@@ -8,8 +9,8 @@ from ..utils import (
|
|||||||
clean_html,
|
clean_html,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
|
try_get,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,8 +25,9 @@ class CCMAIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'L\'espot de La Marató de TV3',
|
'title': 'L\'espot de La Marató de TV3',
|
||||||
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
||||||
'timestamp': 1470918540,
|
'timestamp': 1478608140,
|
||||||
'upload_date': '20160811',
|
'upload_date': '20161108',
|
||||||
|
'age_limit': 0,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ccma.cat/catradio/alacarta/programa/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
'url': 'http://www.ccma.cat/catradio/alacarta/programa/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
||||||
@@ -35,8 +37,24 @@ class CCMAIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'El Consell de Savis analitza el derbi',
|
'title': 'El Consell de Savis analitza el derbi',
|
||||||
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
||||||
'upload_date': '20171205',
|
'upload_date': '20170512',
|
||||||
'timestamp': 1512507300,
|
'timestamp': 1494622500,
|
||||||
|
'vcodec': 'none',
|
||||||
|
'categories': ['Esports'],
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.ccma.cat/tv3/alacarta/crims/crims-josep-tallada-lespereu-me-capitol-1/video/6031387/',
|
||||||
|
'md5': 'b43c3d3486f430f3032b5b160d80cbc3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6031387',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Crims - Josep Talleda, l\'"Espereu-me" (capítol 1)',
|
||||||
|
'description': 'md5:7cbdafb640da9d0d2c0f62bad1e74e60',
|
||||||
|
'timestamp': 1582577700,
|
||||||
|
'upload_date': '20200224',
|
||||||
|
'subtitles': 'mincount:4',
|
||||||
|
'age_limit': 16,
|
||||||
|
'series': 'Crims',
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -72,17 +90,27 @@ class CCMAIE(InfoExtractor):
|
|||||||
|
|
||||||
informacio = media['informacio']
|
informacio = media['informacio']
|
||||||
title = informacio['titol']
|
title = informacio['titol']
|
||||||
durada = informacio.get('durada', {})
|
durada = informacio.get('durada') or {}
|
||||||
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
||||||
timestamp = parse_iso8601(informacio.get('data_emissio', {}).get('utc'))
|
tematica = try_get(informacio, lambda x: x['tematica']['text'])
|
||||||
|
|
||||||
|
timestamp = None
|
||||||
|
data_utc = try_get(informacio, lambda x: x['data_emissio']['utc'])
|
||||||
|
try:
|
||||||
|
timestamp = datetime.datetime.strptime(
|
||||||
|
data_utc, '%Y-%d-%mT%H:%M:%S%z').timestamp()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
subtitols = media.get('subtitols', {})
|
subtitols = media.get('subtitols') or []
|
||||||
if subtitols:
|
if isinstance(subtitols, dict):
|
||||||
sub_url = subtitols.get('url')
|
subtitols = [subtitols]
|
||||||
|
for st in subtitols:
|
||||||
|
sub_url = st.get('url')
|
||||||
if sub_url:
|
if sub_url:
|
||||||
subtitles.setdefault(
|
subtitles.setdefault(
|
||||||
subtitols.get('iso') or subtitols.get('text') or 'ca', []).append({
|
st.get('iso') or st.get('text') or 'ca', []).append({
|
||||||
'url': sub_url,
|
'url': sub_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,6 +125,16 @@ class CCMAIE(InfoExtractor):
|
|||||||
'height': int_or_none(imatges.get('alcada')),
|
'height': int_or_none(imatges.get('alcada')),
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
age_limit = None
|
||||||
|
codi_etic = try_get(informacio, lambda x: x['codi_etic']['id'])
|
||||||
|
if codi_etic:
|
||||||
|
codi_etic_s = codi_etic.split('_')
|
||||||
|
if len(codi_etic_s) == 2:
|
||||||
|
if codi_etic_s[1] == 'TP':
|
||||||
|
age_limit = 0
|
||||||
|
else:
|
||||||
|
age_limit = int_or_none(codi_etic_s[1])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': media_id,
|
'id': media_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
@@ -106,4 +144,9 @@ class CCMAIE(InfoExtractor):
|
|||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'age_limit': age_limit,
|
||||||
|
'alt_title': informacio.get('titol_complet'),
|
||||||
|
'episode_number': int_or_none(informacio.get('capitol')),
|
||||||
|
'categories': [tematica] if tematica else None,
|
||||||
|
'series': informacio.get('programa'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class CDAIE(InfoExtractor):
|
|||||||
raise ExtractorError('This video is only available for premium users.', expected=True)
|
raise ExtractorError('This video is only available for premium users.', expected=True)
|
||||||
|
|
||||||
need_confirm_age = False
|
need_confirm_age = False
|
||||||
if self._html_search_regex(r'(<form[^>]+action="/a/validatebirth")',
|
if self._html_search_regex(r'(<form[^>]+action="[^"]*/a/validatebirth[^"]*")',
|
||||||
webpage, 'birthday validate form', default=None):
|
webpage, 'birthday validate form', default=None):
|
||||||
webpage = self._download_age_confirm_page(
|
webpage = self._download_age_confirm_page(
|
||||||
url, video_id, note='Confirming age')
|
url, video_id, note='Confirming age')
|
||||||
|
|||||||
@@ -336,9 +336,8 @@ class InfoExtractor(object):
|
|||||||
There must be a key "entries", which is a list, an iterable, or a PagedList
|
There must be a key "entries", which is a list, an iterable, or a PagedList
|
||||||
object, each element of which is a valid dictionary by this specification.
|
object, each element of which is a valid dictionary by this specification.
|
||||||
|
|
||||||
Additionally, playlists can have "id", "title", "description", "uploader",
|
Additionally, playlists can have "id", "title", and any other relevent
|
||||||
"uploader_id", "uploader_url", "duration" attributes with the same semantics
|
attributes with the same semantics as videos (see above).
|
||||||
as videos (see above).
|
|
||||||
|
|
||||||
|
|
||||||
_type "multi_video" indicates that there are multiple videos that
|
_type "multi_video" indicates that there are multiple videos that
|
||||||
@@ -967,10 +966,11 @@ class InfoExtractor(object):
|
|||||||
urls, playlist_id=playlist_id, playlist_title=playlist_title)
|
urls, playlist_id=playlist_id, playlist_title=playlist_title)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None):
|
def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None, **kwargs):
|
||||||
"""Returns a playlist"""
|
"""Returns a playlist"""
|
||||||
video_info = {'_type': 'playlist',
|
video_info = {'_type': 'playlist',
|
||||||
'entries': entries}
|
'entries': entries}
|
||||||
|
video_info.update(kwargs)
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
video_info['id'] = playlist_id
|
video_info['id'] = playlist_id
|
||||||
if playlist_title:
|
if playlist_title:
|
||||||
@@ -1366,13 +1366,13 @@ class InfoExtractor(object):
|
|||||||
class FormatSort:
|
class FormatSort:
|
||||||
regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<seperator>[~:])(?P<limit>.*?))?)? *$'
|
regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<seperator>[~:])(?P<limit>.*?))?)? *$'
|
||||||
|
|
||||||
default = ('hidden', 'has_video', 'extractor', 'lang', 'quality',
|
default = ('hidden', 'hasvid', 'ie_pref', 'lang', 'quality',
|
||||||
'res', 'fps', 'codec', 'size', 'br', 'asr',
|
'res', 'fps', 'codec:vp9', 'size', 'br', 'asr',
|
||||||
'proto', 'ext', 'has_audio', 'source', 'format_id')
|
'proto', 'ext', 'has_audio', 'source', 'format_id') # These must not be aliases
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
'vcodec': {'type': 'ordered', 'regex': True,
|
'vcodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['vp9', '(h265|he?vc?)', '(h264|avc)', 'vp8', '(mp4v|h263)', 'theora', '', None, 'none']},
|
'order': ['av0?1', 'vp9', '(h265|he?vc?)', '(h264|avc)', 'vp8', '(mp4v|h263)', 'theora', '', None, 'none']},
|
||||||
'acodec': {'type': 'ordered', 'regex': True,
|
'acodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
|
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
|
||||||
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||||
@@ -2264,7 +2264,7 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}, data=None, headers={}, query={}):
|
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
|
||||||
res = self._download_xml_handle(
|
res = self._download_xml_handle(
|
||||||
mpd_url, video_id,
|
mpd_url, video_id,
|
||||||
note=note or 'Downloading MPD manifest',
|
note=note or 'Downloading MPD manifest',
|
||||||
@@ -2278,10 +2278,9 @@ class InfoExtractor(object):
|
|||||||
mpd_base_url = base_url(urlh.geturl())
|
mpd_base_url = base_url(urlh.geturl())
|
||||||
|
|
||||||
return self._parse_mpd_formats(
|
return self._parse_mpd_formats(
|
||||||
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url,
|
mpd_doc, mpd_id, mpd_base_url, mpd_url)
|
||||||
formats_dict=formats_dict, mpd_url=mpd_url)
|
|
||||||
|
|
||||||
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
|
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', mpd_url=None):
|
||||||
"""
|
"""
|
||||||
Parse formats from MPD manifest.
|
Parse formats from MPD manifest.
|
||||||
References:
|
References:
|
||||||
@@ -2560,15 +2559,7 @@ class InfoExtractor(object):
|
|||||||
else:
|
else:
|
||||||
# Assuming direct URL to unfragmented media.
|
# Assuming direct URL to unfragmented media.
|
||||||
f['url'] = base_url
|
f['url'] = base_url
|
||||||
|
formats.append(f)
|
||||||
# According to [1, 5.3.5.2, Table 7, page 35] @id of Representation
|
|
||||||
# is not necessarily unique within a Period thus formats with
|
|
||||||
# the same `format_id` are quite possible. There are numerous examples
|
|
||||||
# of such manifests (see https://github.com/ytdl-org/youtube-dl/issues/15111,
|
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/13919)
|
|
||||||
full_info = formats_dict.get(representation_id, {}).copy()
|
|
||||||
full_info.update(f)
|
|
||||||
formats.append(full_info)
|
|
||||||
else:
|
else:
|
||||||
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
||||||
return formats
|
return formats
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EggheadCourseIE(InfoExtractor):
|
class EggheadBaseIE(InfoExtractor):
|
||||||
|
def _call_api(self, path, video_id, resource, fatal=True):
|
||||||
|
return self._download_json(
|
||||||
|
'https://app.egghead.io/api/v1/' + path,
|
||||||
|
video_id, 'Downloading %s JSON' % resource, fatal=fatal)
|
||||||
|
|
||||||
|
|
||||||
|
class EggheadCourseIE(EggheadBaseIE):
|
||||||
IE_DESC = 'egghead.io course'
|
IE_DESC = 'egghead.io course'
|
||||||
IE_NAME = 'egghead:course'
|
IE_NAME = 'egghead:course'
|
||||||
_VALID_URL = r'https://egghead\.io/courses/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https://egghead\.io/courses/(?P<id>[^/?#&]+)'
|
||||||
@@ -28,10 +35,9 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
playlist_id = self._match_id(url)
|
playlist_id = self._match_id(url)
|
||||||
|
series_path = 'series/' + playlist_id
|
||||||
lessons = self._download_json(
|
lessons = self._call_api(
|
||||||
'https://egghead.io/api/v1/series/%s/lessons' % playlist_id,
|
series_path + '/lessons', playlist_id, 'course lessons')
|
||||||
playlist_id, 'Downloading course lessons JSON')
|
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for lesson in lessons:
|
for lesson in lessons:
|
||||||
@@ -44,9 +50,8 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
lesson_url, ie=EggheadLessonIE.ie_key(), video_id=lesson_id))
|
lesson_url, ie=EggheadLessonIE.ie_key(), video_id=lesson_id))
|
||||||
|
|
||||||
course = self._download_json(
|
course = self._call_api(
|
||||||
'https://egghead.io/api/v1/series/%s' % playlist_id,
|
series_path, playlist_id, 'course', False) or {}
|
||||||
playlist_id, 'Downloading course JSON', fatal=False) or {}
|
|
||||||
|
|
||||||
playlist_id = course.get('id')
|
playlist_id = course.get('id')
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
@@ -57,7 +62,7 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
course.get('description'))
|
course.get('description'))
|
||||||
|
|
||||||
|
|
||||||
class EggheadLessonIE(InfoExtractor):
|
class EggheadLessonIE(EggheadBaseIE):
|
||||||
IE_DESC = 'egghead.io lesson'
|
IE_DESC = 'egghead.io lesson'
|
||||||
IE_NAME = 'egghead:lesson'
|
IE_NAME = 'egghead:lesson'
|
||||||
_VALID_URL = r'https://egghead\.io/(?:api/v1/)?lessons/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https://egghead\.io/(?:api/v1/)?lessons/(?P<id>[^/?#&]+)'
|
||||||
@@ -74,7 +79,7 @@ class EggheadLessonIE(InfoExtractor):
|
|||||||
'upload_date': '20161209',
|
'upload_date': '20161209',
|
||||||
'duration': 304,
|
'duration': 304,
|
||||||
'view_count': 0,
|
'view_count': 0,
|
||||||
'tags': ['javascript', 'free'],
|
'tags': 'count:2',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@@ -88,8 +93,8 @@ class EggheadLessonIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
lesson = self._download_json(
|
lesson = self._call_api(
|
||||||
'https://egghead.io/api/v1/lessons/%s' % display_id, display_id)
|
'lessons/' + display_id, display_id, 'lesson')
|
||||||
|
|
||||||
lesson_id = compat_str(lesson['id'])
|
lesson_id = compat_str(lesson['id'])
|
||||||
title = lesson['title']
|
title = lesson['title']
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ from .atvat import ATVAtIE
|
|||||||
from .audimedia import AudiMediaIE
|
from .audimedia import AudiMediaIE
|
||||||
from .audioboom import AudioBoomIE
|
from .audioboom import AudioBoomIE
|
||||||
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
||||||
from .audius import AudiusIE, AudiusPlaylistIE
|
from .audius import (
|
||||||
|
AudiusIE,
|
||||||
|
AudiusTrackIE,
|
||||||
|
AudiusPlaylistIE
|
||||||
|
)
|
||||||
from .awaan import (
|
from .awaan import (
|
||||||
AWAANIE,
|
AWAANIE,
|
||||||
AWAANVideoIE,
|
AWAANVideoIE,
|
||||||
@@ -1304,6 +1308,7 @@ from .tv2 import (
|
|||||||
TV2IE,
|
TV2IE,
|
||||||
TV2ArticleIE,
|
TV2ArticleIE,
|
||||||
KatsomoIE,
|
KatsomoIE,
|
||||||
|
MTVUutisetArticleIE,
|
||||||
)
|
)
|
||||||
from .tv2dk import (
|
from .tv2dk import (
|
||||||
TV2DKIE,
|
TV2DKIE,
|
||||||
@@ -1444,7 +1449,6 @@ from .vidme import (
|
|||||||
VidmeUserIE,
|
VidmeUserIE,
|
||||||
VidmeUserLikesIE,
|
VidmeUserLikesIE,
|
||||||
)
|
)
|
||||||
from .vidzi import VidziIE
|
|
||||||
from .vier import VierIE, VierVideosIE
|
from .vier import VierIE, VierVideosIE
|
||||||
from .viewlift import (
|
from .viewlift import (
|
||||||
ViewLiftIE,
|
ViewLiftIE,
|
||||||
@@ -1504,6 +1508,7 @@ from .vrv import (
|
|||||||
VRVSeriesIE,
|
VRVSeriesIE,
|
||||||
)
|
)
|
||||||
from .vshare import VShareIE
|
from .vshare import VShareIE
|
||||||
|
from .vtm import VTMIE
|
||||||
from .medialaan import MedialaanIE
|
from .medialaan import MedialaanIE
|
||||||
from .vube import VubeIE
|
from .vube import VubeIE
|
||||||
from .vuclip import VuClipIE
|
from .vuclip import VuClipIE
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ from .gedi import GediEmbedsIE
|
|||||||
from .rcs import RCSEmbedsIE
|
from .rcs import RCSEmbedsIE
|
||||||
from .bitchute import BitChuteIE
|
from .bitchute import BitChuteIE
|
||||||
from .arcpublishing import ArcPublishingIE
|
from .arcpublishing import ArcPublishingIE
|
||||||
|
from .medialaan import MedialaanIE
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
@@ -2224,6 +2225,20 @@ class GenericIE(InfoExtractor):
|
|||||||
'duration': 1581,
|
'duration': 1581,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# MyChannels SDK embed
|
||||||
|
# https://www.24kitchen.nl/populair/deskundige-dit-waarom-sommigen-gevoelig-zijn-voor-voedselallergieen
|
||||||
|
'url': 'https://www.demorgen.be/nieuws/burgemeester-rotterdam-richt-zich-in-videoboodschap-tot-relschoppers-voelt-het-goed~b0bcfd741/',
|
||||||
|
'md5': '90c0699c37006ef18e198c032d81739c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '194165',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Burgemeester Aboutaleb spreekt relschoppers toe',
|
||||||
|
'timestamp': 1611740340,
|
||||||
|
'upload_date': '20210127',
|
||||||
|
'duration': 159,
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def report_following_redirect(self, new_url):
|
def report_following_redirect(self, new_url):
|
||||||
@@ -2463,6 +2478,9 @@ class GenericIE(InfoExtractor):
|
|||||||
webpage = self._webpage_read_content(
|
webpage = self._webpage_read_content(
|
||||||
full_response, url, video_id, prefix=first_bytes)
|
full_response, url, video_id, prefix=first_bytes)
|
||||||
|
|
||||||
|
if '<title>DPG Media Privacy Gate</title>' in webpage:
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
self.report_extraction(video_id)
|
self.report_extraction(video_id)
|
||||||
|
|
||||||
# Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest?
|
# Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest?
|
||||||
@@ -2594,6 +2612,11 @@ class GenericIE(InfoExtractor):
|
|||||||
if arc_urls:
|
if arc_urls:
|
||||||
return self.playlist_from_matches(arc_urls, video_id, video_title, ie=ArcPublishingIE.ie_key())
|
return self.playlist_from_matches(arc_urls, video_id, video_title, ie=ArcPublishingIE.ie_key())
|
||||||
|
|
||||||
|
mychannels_urls = MedialaanIE._extract_urls(webpage)
|
||||||
|
if mychannels_urls:
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
mychannels_urls, video_id, video_title, ie=MedialaanIE.ie_key())
|
||||||
|
|
||||||
# Look for embedded rtl.nl player
|
# Look for embedded rtl.nl player
|
||||||
matches = re.findall(
|
matches = re.findall(
|
||||||
r'<iframe[^>]+?src="((?:https?:)?//(?:(?:www|static)\.)?rtl\.nl/(?:system/videoplayer/[^"]+(?:video_)?)?embed[^"]+)"',
|
r'<iframe[^>]+?src="((?:https?:)?//(?:(?:www|static)\.)?rtl\.nl/(?:system/videoplayer/[^"]+(?:video_)?)?embed[^"]+)"',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from ..compat import compat_parse_qs
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -237,7 +238,7 @@ class GoogleDriveIE(InfoExtractor):
|
|||||||
if confirmation_webpage:
|
if confirmation_webpage:
|
||||||
confirm = self._search_regex(
|
confirm = self._search_regex(
|
||||||
r'confirm=([^&"\']+)', confirmation_webpage,
|
r'confirm=([^&"\']+)', confirmation_webpage,
|
||||||
'confirmation code', fatal=False)
|
'confirmation code', default=None)
|
||||||
if confirm:
|
if confirm:
|
||||||
confirmed_source_url = update_url_query(source_url, {
|
confirmed_source_url = update_url_query(source_url, {
|
||||||
'confirm': confirm,
|
'confirm': confirm,
|
||||||
@@ -245,6 +246,11 @@ class GoogleDriveIE(InfoExtractor):
|
|||||||
urlh = request_source_file(confirmed_source_url, 'confirmed source')
|
urlh = request_source_file(confirmed_source_url, 'confirmed source')
|
||||||
if urlh and urlh.headers.get('Content-Disposition'):
|
if urlh and urlh.headers.get('Content-Disposition'):
|
||||||
add_source_format(urlh)
|
add_source_format(urlh)
|
||||||
|
else:
|
||||||
|
self.report_warning(
|
||||||
|
get_element_by_class('uc-error-subcaption', confirmation_webpage)
|
||||||
|
or get_element_by_class('uc-error-caption', confirmation_webpage)
|
||||||
|
or 'unable to extract confirmation code')
|
||||||
|
|
||||||
if not formats and reason:
|
if not formats and reason:
|
||||||
raise ExtractorError(reason, expected=True)
|
raise ExtractorError(reason, expected=True)
|
||||||
|
|||||||
@@ -2,268 +2,113 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .gigya import GigyaBaseIE
|
from .common import InfoExtractor
|
||||||
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
extract_attributes,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
mimetype2ext,
|
||||||
try_get,
|
parse_iso8601,
|
||||||
unified_timestamp,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MedialaanIE(GigyaBaseIE):
|
class MedialaanIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.|nieuws\.)?
|
|
||||||
(?:
|
(?:
|
||||||
(?P<site_id>vtm|q2|vtmkzoom)\.be/
|
(?:embed\.)?mychannels.video/embed/|
|
||||||
(?:
|
embed\.mychannels\.video/(?:s(?:dk|cript)/)?production/|
|
||||||
video(?:/[^/]+/id/|/?\?.*?\baid=)|
|
(?:www\.)?(?:
|
||||||
(?:[^/]+/)*
|
(?:
|
||||||
)
|
7sur7|
|
||||||
|
demorgen|
|
||||||
|
hln|
|
||||||
|
joe|
|
||||||
|
qmusic
|
||||||
|
)\.be|
|
||||||
|
(?:
|
||||||
|
[abe]d|
|
||||||
|
bndestem|
|
||||||
|
destentor|
|
||||||
|
gelderlander|
|
||||||
|
pzc|
|
||||||
|
tubantia|
|
||||||
|
volkskrant
|
||||||
|
)\.nl
|
||||||
|
)/video/(?:[^/]+/)*[^/?&#]+~p
|
||||||
)
|
)
|
||||||
(?P<id>[^/?#&]+)
|
(?P<id>\d+)
|
||||||
'''
|
'''
|
||||||
_NETRC_MACHINE = 'medialaan'
|
|
||||||
_APIKEY = '3_HZ0FtkMW_gOyKlqQzW5_0FHRC7Nd5XpXJZcDdXY4pk5eES2ZWmejRW5egwVm4ug-'
|
|
||||||
_SITE_TO_APP_ID = {
|
|
||||||
'vtm': 'vtm_watch',
|
|
||||||
'q2': 'q2',
|
|
||||||
'vtmkzoom': 'vtmkzoom',
|
|
||||||
}
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# vod
|
'url': 'https://www.bndestem.nl/video/de-terugkeer-van-ally-de-aap-en-wie-vertrekt-er-nog-bij-nac~p193993',
|
||||||
'url': 'http://vtm.be/video/volledige-afleveringen/id/vtm_20170219_VM0678361_vtmwatch',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'vtm_20170219_VM0678361_vtmwatch',
|
'id': '193993',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Allemaal Chris afl. 6',
|
'title': 'De terugkeer van Ally de Aap en wie vertrekt er nog bij NAC?',
|
||||||
'description': 'md5:4be86427521e7b07e0adb0c9c554ddb2',
|
'timestamp': 1611663540,
|
||||||
'timestamp': 1487533280,
|
'upload_date': '20210126',
|
||||||
'upload_date': '20170219',
|
'duration': 238,
|
||||||
'duration': 2562,
|
|
||||||
'series': 'Allemaal Chris',
|
|
||||||
'season': 'Allemaal Chris',
|
|
||||||
'season_number': 1,
|
|
||||||
'season_id': '256936078124527',
|
|
||||||
'episode': 'Allemaal Chris afl. 6',
|
|
||||||
'episode_number': 6,
|
|
||||||
'episode_id': '256936078591527',
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'skip': 'Requires account credentials',
|
|
||||||
}, {
|
}, {
|
||||||
# clip
|
'url': 'https://www.gelderlander.nl/video/kanalen/degelderlander~c320/series/snel-nieuws~s984/noodbevel-in-doetinchem-politie-stuurt-mensen-centrum-uit~p194093',
|
||||||
'url': 'http://vtm.be/video?aid=168332',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '168332',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '"Veronique liegt!"',
|
|
||||||
'description': 'md5:1385e2b743923afe54ba4adc38476155',
|
|
||||||
'timestamp': 1489002029,
|
|
||||||
'upload_date': '20170308',
|
|
||||||
'duration': 96,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# vod
|
|
||||||
'url': 'http://vtm.be/video/volledige-afleveringen/id/257107153551000',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# vod
|
'url': 'https://embed.mychannels.video/sdk/production/193993?options=TFTFF_default',
|
||||||
'url': 'http://vtm.be/video?aid=163157',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# vod
|
'url': 'https://embed.mychannels.video/script/production/193993',
|
||||||
'url': 'http://www.q2.be/video/volledige-afleveringen/id/2be_20170301_VM0684442_q2',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# clip
|
'url': 'https://embed.mychannels.video/production/193993',
|
||||||
'url': 'http://vtmkzoom.be/k3-dansstudio/een-nieuw-seizoen-van-k3-dansstudio',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# http/s redirect
|
'url': 'https://mychannels.video/embed/193993',
|
||||||
'url': 'https://vtmkzoom.be/video?aid=45724',
|
'only_matching': True,
|
||||||
'info_dict': {
|
|
||||||
'id': '257136373657000',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'K3 Dansstudio Ushuaia afl.6',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Requires account credentials',
|
|
||||||
}, {
|
}, {
|
||||||
# nieuws.vtm.be
|
'url': 'https://embed.mychannels.video/embed/193993',
|
||||||
'url': 'https://nieuws.vtm.be/stadion/stadion/genk-nog-moeilijk-programma',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_initialize(self):
|
@staticmethod
|
||||||
self._logged_in = False
|
def _extract_urls(webpage):
|
||||||
|
entries = []
|
||||||
def _login(self):
|
for element in re.findall(r'(<div[^>]+data-mychannels-type="video"[^>]*>)', webpage):
|
||||||
username, password = self._get_login_info()
|
mychannels_id = extract_attributes(element).get('data-mychannels-id')
|
||||||
if username is None:
|
if mychannels_id:
|
||||||
self.raise_login_required()
|
entries.append('https://mychannels.video/embed/' + mychannels_id)
|
||||||
|
return entries
|
||||||
auth_data = {
|
|
||||||
'APIKey': self._APIKEY,
|
|
||||||
'sdk': 'js_6.1',
|
|
||||||
'format': 'json',
|
|
||||||
'loginID': username,
|
|
||||||
'password': password,
|
|
||||||
}
|
|
||||||
|
|
||||||
auth_info = self._gigya_login(auth_data)
|
|
||||||
|
|
||||||
self._uid = auth_info['UID']
|
|
||||||
self._uid_signature = auth_info['UIDSignature']
|
|
||||||
self._signature_timestamp = auth_info['signatureTimestamp']
|
|
||||||
|
|
||||||
self._logged_in = True
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
production_id = self._match_id(url)
|
||||||
video_id, site_id = mobj.group('id', 'site_id')
|
production = self._download_json(
|
||||||
|
'https://embed.mychannels.video/sdk/production/' + production_id,
|
||||||
|
production_id, query={'options': 'UUUU_default'})['productions'][0]
|
||||||
|
title = production['title']
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
formats = []
|
||||||
|
for source in (production.get('sources') or []):
|
||||||
config = self._parse_json(
|
src = source.get('src')
|
||||||
self._search_regex(
|
if not src:
|
||||||
r'videoJSConfig\s*=\s*JSON\.parse\(\'({.+?})\'\);',
|
continue
|
||||||
webpage, 'config', default='{}'), video_id,
|
ext = mimetype2ext(source.get('type'))
|
||||||
transform_source=lambda s: s.replace(
|
if ext == 'm3u8':
|
||||||
'\\\\', '\\').replace(r'\"', '"').replace(r"\'", "'"))
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
src, production_id, 'mp4', 'm3u8_native',
|
||||||
vod_id = config.get('vodId') or self._search_regex(
|
m3u8_id='hls', fatal=False))
|
||||||
(r'\\"vodId\\"\s*:\s*\\"(.+?)\\"',
|
|
||||||
r'"vodId"\s*:\s*"(.+?)"',
|
|
||||||
r'<[^>]+id=["\']vod-(\d+)'),
|
|
||||||
webpage, 'video_id', default=None)
|
|
||||||
|
|
||||||
# clip, no authentication required
|
|
||||||
if not vod_id:
|
|
||||||
player = self._parse_json(
|
|
||||||
self._search_regex(
|
|
||||||
r'vmmaplayer\(({.+?})\);', webpage, 'vmma player',
|
|
||||||
default=''),
|
|
||||||
video_id, transform_source=lambda s: '[%s]' % s, fatal=False)
|
|
||||||
if player:
|
|
||||||
video = player[-1]
|
|
||||||
if video['videoUrl'] in ('http', 'https'):
|
|
||||||
return self.url_result(video['url'], MedialaanIE.ie_key())
|
|
||||||
info = {
|
|
||||||
'id': video_id,
|
|
||||||
'url': video['videoUrl'],
|
|
||||||
'title': video['title'],
|
|
||||||
'thumbnail': video.get('imageUrl'),
|
|
||||||
'timestamp': int_or_none(video.get('createdDate')),
|
|
||||||
'duration': int_or_none(video.get('duration')),
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
info = self._parse_html5_media_entries(
|
formats.append({
|
||||||
url, webpage, video_id, m3u8_id='hls')[0]
|
'ext': ext,
|
||||||
info.update({
|
'url': src,
|
||||||
'id': video_id,
|
|
||||||
'title': self._html_search_meta('description', webpage),
|
|
||||||
'duration': parse_duration(self._html_search_meta('duration', webpage)),
|
|
||||||
})
|
})
|
||||||
# vod, authentication required
|
self._sort_formats(formats)
|
||||||
else:
|
|
||||||
if not self._logged_in:
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
settings = self._parse_json(
|
return {
|
||||||
self._search_regex(
|
'id': production_id,
|
||||||
r'jQuery\.extend\(Drupal\.settings\s*,\s*({.+?})\);',
|
'title': title,
|
||||||
webpage, 'drupal settings', default='{}'),
|
'formats': formats,
|
||||||
video_id)
|
'thumbnail': production.get('posterUrl'),
|
||||||
|
'timestamp': parse_iso8601(production.get('publicationDate'), ' '),
|
||||||
def get(container, item):
|
'duration': int_or_none(production.get('duration')) or None,
|
||||||
return try_get(
|
}
|
||||||
settings, lambda x: x[container][item],
|
|
||||||
compat_str) or self._search_regex(
|
|
||||||
r'"%s"\s*:\s*"([^"]+)' % item, webpage, item,
|
|
||||||
default=None)
|
|
||||||
|
|
||||||
app_id = get('vod', 'app_id') or self._SITE_TO_APP_ID.get(site_id, 'vtm_watch')
|
|
||||||
sso = get('vod', 'gigyaDatabase') or 'vtm-sso'
|
|
||||||
|
|
||||||
data = self._download_json(
|
|
||||||
'http://vod.medialaan.io/api/1.0/item/%s/video' % vod_id,
|
|
||||||
video_id, query={
|
|
||||||
'app_id': app_id,
|
|
||||||
'user_network': sso,
|
|
||||||
'UID': self._uid,
|
|
||||||
'UIDSignature': self._uid_signature,
|
|
||||||
'signatureTimestamp': self._signature_timestamp,
|
|
||||||
})
|
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
data['response']['uri'], video_id, entry_protocol='m3u8_native',
|
|
||||||
ext='mp4', m3u8_id='hls')
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'id': vod_id,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
api_key = get('vod', 'apiKey')
|
|
||||||
channel = get('medialaanGigya', 'channel')
|
|
||||||
|
|
||||||
if api_key:
|
|
||||||
videos = self._download_json(
|
|
||||||
'http://vod.medialaan.io/vod/v2/videos', video_id, fatal=False,
|
|
||||||
query={
|
|
||||||
'channels': channel,
|
|
||||||
'ids': vod_id,
|
|
||||||
'limit': 1,
|
|
||||||
'apikey': api_key,
|
|
||||||
})
|
|
||||||
if videos:
|
|
||||||
video = try_get(
|
|
||||||
videos, lambda x: x['response']['videos'][0], dict)
|
|
||||||
if video:
|
|
||||||
def get(container, item, expected_type=None):
|
|
||||||
return try_get(
|
|
||||||
video, lambda x: x[container][item], expected_type)
|
|
||||||
|
|
||||||
def get_string(container, item):
|
|
||||||
return get(container, item, compat_str)
|
|
||||||
|
|
||||||
info.update({
|
|
||||||
'series': get_string('program', 'title'),
|
|
||||||
'season': get_string('season', 'title'),
|
|
||||||
'season_number': int_or_none(get('season', 'number')),
|
|
||||||
'season_id': get_string('season', 'id'),
|
|
||||||
'episode': get_string('episode', 'title'),
|
|
||||||
'episode_number': int_or_none(get('episode', 'number')),
|
|
||||||
'episode_id': get_string('episode', 'id'),
|
|
||||||
'duration': int_or_none(
|
|
||||||
video.get('duration')) or int_or_none(
|
|
||||||
video.get('durationMillis'), scale=1000),
|
|
||||||
'title': get_string('episode', 'title'),
|
|
||||||
'description': get_string('episode', 'text'),
|
|
||||||
'timestamp': unified_timestamp(get_string(
|
|
||||||
'publication', 'begin')),
|
|
||||||
})
|
|
||||||
|
|
||||||
if not info.get('title'):
|
|
||||||
info['title'] = try_get(
|
|
||||||
config, lambda x: x['videoConfig']['title'],
|
|
||||||
compat_str) or self._html_search_regex(
|
|
||||||
r'\\"title\\"\s*:\s*\\"(.+?)\\"', webpage, 'title',
|
|
||||||
default=None) or self._og_search_title(webpage)
|
|
||||||
|
|
||||||
if not info.get('description'):
|
|
||||||
info['description'] = self._html_search_regex(
|
|
||||||
r'<div[^>]+class="field-item\s+even">\s*<p>(.+?)</p>',
|
|
||||||
webpage, 'description', default=None)
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ from ..utils import (
|
|||||||
orderedSet,
|
orderedSet,
|
||||||
remove_quotes,
|
remove_quotes,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
|
update_url_query,
|
||||||
|
urlencode_postdata,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PornHubBaseIE(InfoExtractor):
|
class PornHubBaseIE(InfoExtractor):
|
||||||
|
_NETRC_MACHINE = 'pornhub'
|
||||||
|
|
||||||
def _download_webpage_handle(self, *args, **kwargs):
|
def _download_webpage_handle(self, *args, **kwargs):
|
||||||
def dl(*args, **kwargs):
|
def dl(*args, **kwargs):
|
||||||
return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)
|
return super(PornHubBaseIE, self)._download_webpage_handle(*args, **kwargs)
|
||||||
@@ -52,6 +56,66 @@ class PornHubBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
return webpage, urlh
|
return webpage, urlh
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._logged_in = False
|
||||||
|
|
||||||
|
def _login(self, host):
|
||||||
|
if self._logged_in:
|
||||||
|
return
|
||||||
|
|
||||||
|
site = host.split('.')[0]
|
||||||
|
|
||||||
|
# Both sites pornhub and pornhubpremium have separate accounts
|
||||||
|
# so there should be an option to provide credentials for both.
|
||||||
|
# At the same time some videos are available under the same video id
|
||||||
|
# on both sites so that we have to identify them as the same video.
|
||||||
|
# For that purpose we have to keep both in the same extractor
|
||||||
|
# but under different netrc machines.
|
||||||
|
username, password = self._get_login_info(netrc_machine=site)
|
||||||
|
if username is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_url = 'https://www.%s/%slogin' % (host, 'premium/' if 'premium' in host else '')
|
||||||
|
login_page = self._download_webpage(
|
||||||
|
login_url, None, 'Downloading %s login page' % site)
|
||||||
|
|
||||||
|
def is_logged(webpage):
|
||||||
|
return any(re.search(p, webpage) for p in (
|
||||||
|
r'class=["\']signOut',
|
||||||
|
r'>Sign\s+[Oo]ut\s*<'))
|
||||||
|
|
||||||
|
if is_logged(login_page):
|
||||||
|
self._logged_in = True
|
||||||
|
return
|
||||||
|
|
||||||
|
login_form = self._hidden_inputs(login_page)
|
||||||
|
|
||||||
|
login_form.update({
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
})
|
||||||
|
|
||||||
|
response = self._download_json(
|
||||||
|
'https://www.%s/front/authenticate' % host, None,
|
||||||
|
'Logging in to %s' % site,
|
||||||
|
data=urlencode_postdata(login_form),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'Referer': login_url,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
})
|
||||||
|
|
||||||
|
if response.get('success') == '1':
|
||||||
|
self._logged_in = True
|
||||||
|
return
|
||||||
|
|
||||||
|
message = response.get('message')
|
||||||
|
if message is not None:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Unable to login: %s' % message, expected=True)
|
||||||
|
|
||||||
|
raise ExtractorError('Unable to log in')
|
||||||
|
|
||||||
|
|
||||||
class PornHubIE(PornHubBaseIE):
|
class PornHubIE(PornHubBaseIE):
|
||||||
IE_DESC = 'PornHub and Thumbzilla'
|
IE_DESC = 'PornHub and Thumbzilla'
|
||||||
@@ -163,12 +227,20 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5e4acdae54a82',
|
'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5e4acdae54a82',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Some videos are available with the same id on both premium
|
||||||
|
# and non-premium sites (e.g. this and the following test)
|
||||||
|
'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5f75b0f4b18e3',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5f75b0f4b18e3',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_urls(webpage):
|
def _extract_urls(webpage):
|
||||||
return re.findall(
|
return re.findall(
|
||||||
r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub\.(?:com|net|org)/embed/[\da-z]+)',
|
r'<iframe[^>]+?src=["\'](?P<url>(?:https?:)?//(?:www\.)?pornhub(?:premium)?\.(?:com|net|org)/embed/[\da-z]+)',
|
||||||
webpage)
|
webpage)
|
||||||
|
|
||||||
def _extract_count(self, pattern, webpage, name):
|
def _extract_count(self, pattern, webpage, name):
|
||||||
@@ -180,12 +252,7 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
host = mobj.group('host') or 'pornhub.com'
|
host = mobj.group('host') or 'pornhub.com'
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
|
|
||||||
if 'premium' in host:
|
self._login(host)
|
||||||
if not self._downloader.params.get('cookiefile'):
|
|
||||||
raise ExtractorError(
|
|
||||||
'PornHub Premium requires authentication.'
|
|
||||||
' You may want to use --cookies.',
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
self._set_cookie(host, 'age_verified', '1')
|
self._set_cookie(host, 'age_verified', '1')
|
||||||
|
|
||||||
@@ -405,6 +472,10 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class PornHubPlaylistBaseIE(PornHubBaseIE):
|
class PornHubPlaylistBaseIE(PornHubBaseIE):
|
||||||
|
def _extract_page(self, url):
|
||||||
|
return int_or_none(self._search_regex(
|
||||||
|
r'\bpage=(\d+)', url, 'page', default=None))
|
||||||
|
|
||||||
def _extract_entries(self, webpage, host):
|
def _extract_entries(self, webpage, host):
|
||||||
# Only process container div with main playlist content skipping
|
# Only process container div with main playlist content skipping
|
||||||
# drop-down menu that uses similar pattern for videos (see
|
# drop-down menu that uses similar pattern for videos (see
|
||||||
@@ -422,26 +493,6 @@ class PornHubPlaylistBaseIE(PornHubBaseIE):
|
|||||||
container))
|
container))
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
host = mobj.group('host')
|
|
||||||
playlist_id = mobj.group('id')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, playlist_id)
|
|
||||||
|
|
||||||
entries = self._extract_entries(webpage, host)
|
|
||||||
|
|
||||||
playlist = self._parse_json(
|
|
||||||
self._search_regex(
|
|
||||||
r'(?:playlistObject|PLAYLIST_VIEW)\s*=\s*({.+?});', webpage,
|
|
||||||
'playlist', default='{}'),
|
|
||||||
playlist_id, fatal=False)
|
|
||||||
title = playlist.get('title') or self._search_regex(
|
|
||||||
r'>Videos\s+in\s+(.+?)\s+[Pp]laylist<', webpage, 'title', fatal=False)
|
|
||||||
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, playlist_id, title, playlist.get('description'))
|
|
||||||
|
|
||||||
|
|
||||||
class PornHubUserIE(PornHubPlaylistBaseIE):
|
class PornHubUserIE(PornHubPlaylistBaseIE):
|
||||||
_VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net|org))/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/?#&]+))(?:[?#&]|/(?!videos)|$)'
|
_VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?(?P<host>pornhub(?:premium)?\.(?:com|net|org))/(?:(?:user|channel)s|model|pornstar)/(?P<id>[^/?#&]+))(?:[?#&]|/(?!videos)|$)'
|
||||||
@@ -463,14 +514,27 @@ class PornHubUserIE(PornHubPlaylistBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
|
'url': 'https://www.pornhub.com/model/zoe_ph?abc=1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Unavailable via /videos page, but available with direct pagination
|
||||||
|
# on pornstar page (see [1]), requires premium
|
||||||
|
# 1. https://github.com/ytdl-org/youtube-dl/issues/27853
|
||||||
|
'url': 'https://www.pornhubpremium.com/pornstar/sienna-west',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Same as before, multi page
|
||||||
|
'url': 'https://www.pornhubpremium.com/pornstar/lily-labeau',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
user_id = mobj.group('id')
|
user_id = mobj.group('id')
|
||||||
|
videos_url = '%s/videos' % mobj.group('url')
|
||||||
|
page = self._extract_page(url)
|
||||||
|
if page:
|
||||||
|
videos_url = update_url_query(videos_url, {'page': page})
|
||||||
return self.url_result(
|
return self.url_result(
|
||||||
'%s/videos' % mobj.group('url'), ie=PornHubPagedVideoListIE.ie_key(),
|
videos_url, ie=PornHubPagedVideoListIE.ie_key(), video_id=user_id)
|
||||||
video_id=user_id)
|
|
||||||
|
|
||||||
|
|
||||||
class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
|
class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
|
||||||
@@ -483,32 +547,55 @@ class PornHubPagedPlaylistBaseIE(PornHubPlaylistBaseIE):
|
|||||||
<button[^>]+\bid=["\']moreDataBtn
|
<button[^>]+\bid=["\']moreDataBtn
|
||||||
''', webpage) is not None
|
''', webpage) is not None
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _entries(self, url, host, item_id):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
page = self._extract_page(url)
|
||||||
host = mobj.group('host')
|
|
||||||
item_id = mobj.group('id')
|
|
||||||
|
|
||||||
page = int_or_none(self._search_regex(
|
VIDEOS = '/videos'
|
||||||
r'\bpage=(\d+)', url, 'page', default=None))
|
|
||||||
|
|
||||||
entries = []
|
def download_page(base_url, num, fallback=False):
|
||||||
for page_num in (page, ) if page is not None else itertools.count(1):
|
note = 'Downloading page %d%s' % (num, ' (switch to fallback)' if fallback else '')
|
||||||
|
return self._download_webpage(
|
||||||
|
base_url, item_id, note, query={'page': num})
|
||||||
|
|
||||||
|
def is_404(e):
|
||||||
|
return isinstance(e.cause, compat_HTTPError) and e.cause.code == 404
|
||||||
|
|
||||||
|
base_url = url
|
||||||
|
has_page = page is not None
|
||||||
|
first_page = page if has_page else 1
|
||||||
|
for page_num in (first_page, ) if has_page else itertools.count(first_page):
|
||||||
try:
|
try:
|
||||||
webpage = self._download_webpage(
|
try:
|
||||||
url, item_id, 'Downloading page %d' % page_num,
|
webpage = download_page(base_url, page_num)
|
||||||
query={'page': page_num})
|
except ExtractorError as e:
|
||||||
|
# Some sources may not be available via /videos page,
|
||||||
|
# trying to fallback to main page pagination (see [1])
|
||||||
|
# 1. https://github.com/ytdl-org/youtube-dl/issues/27853
|
||||||
|
if is_404(e) and page_num == first_page and VIDEOS in base_url:
|
||||||
|
base_url = base_url.replace(VIDEOS, '')
|
||||||
|
webpage = download_page(base_url, page_num, fallback=True)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
|
if is_404(e) and page_num != first_page:
|
||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
page_entries = self._extract_entries(webpage, host)
|
page_entries = self._extract_entries(webpage, host)
|
||||||
if not page_entries:
|
if not page_entries:
|
||||||
break
|
break
|
||||||
entries.extend(page_entries)
|
for e in page_entries:
|
||||||
|
yield e
|
||||||
if not self._has_more(webpage):
|
if not self._has_more(webpage):
|
||||||
break
|
break
|
||||||
|
|
||||||
return self.playlist_result(orderedSet(entries), item_id)
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
host = mobj.group('host')
|
||||||
|
item_id = mobj.group('id')
|
||||||
|
|
||||||
|
self._login(host)
|
||||||
|
|
||||||
|
return self.playlist_result(self._entries(url, host, item_id), item_id)
|
||||||
|
|
||||||
|
|
||||||
class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
|
class PornHubPagedVideoListIE(PornHubPagedPlaylistBaseIE):
|
||||||
|
|||||||
@@ -255,8 +255,10 @@ class SVTPlayIE(SVTPlayBaseIE):
|
|||||||
svt_id = self._search_regex(
|
svt_id = self._search_regex(
|
||||||
(r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)',
|
(r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)',
|
||||||
r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)',
|
r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)',
|
||||||
|
r'["\']videoSvtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)',
|
||||||
r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"',
|
r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"',
|
||||||
r'["\']svtId["\']\s*:\s*["\']([\da-zA-Z-]+)'),
|
r'["\']svtId["\']\s*:\s*["\']([\da-zA-Z-]+)',
|
||||||
|
r'["\']svtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)'),
|
||||||
webpage, 'video id')
|
webpage, 'video id')
|
||||||
|
|
||||||
info_dict = self._extract_by_video_id(svt_id, webpage)
|
info_dict = self._extract_by_video_id(svt_id, webpage)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class TV2IE(InfoExtractor):
|
class TV2IE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?tv2\.no/v/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?tv2\.no/v/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.tv2.no/v/916509/',
|
'url': 'http://www.tv2.no/v/916509/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '916509',
|
'id': '916509',
|
||||||
@@ -33,7 +33,7 @@ class TV2IE(InfoExtractor):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
'categories': list,
|
'categories': list,
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
_API_DOMAIN = 'sumo.tv2.no'
|
_API_DOMAIN = 'sumo.tv2.no'
|
||||||
_PROTOCOLS = ('HDS', 'HLS', 'DASH')
|
_PROTOCOLS = ('HDS', 'HLS', 'DASH')
|
||||||
_GEO_COUNTRIES = ['NO']
|
_GEO_COUNTRIES = ['NO']
|
||||||
@@ -42,6 +42,12 @@ class TV2IE(InfoExtractor):
|
|||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
api_base = 'http://%s/api/web/asset/%s' % (self._API_DOMAIN, video_id)
|
api_base = 'http://%s/api/web/asset/%s' % (self._API_DOMAIN, video_id)
|
||||||
|
|
||||||
|
asset = self._download_json(
|
||||||
|
api_base + '.json', video_id,
|
||||||
|
'Downloading metadata JSON')['asset']
|
||||||
|
title = asset.get('subtitle') or asset['title']
|
||||||
|
is_live = asset.get('live') is True
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
format_urls = []
|
format_urls = []
|
||||||
for protocol in self._PROTOCOLS:
|
for protocol in self._PROTOCOLS:
|
||||||
@@ -81,7 +87,8 @@ class TV2IE(InfoExtractor):
|
|||||||
elif ext == 'm3u8':
|
elif ext == 'm3u8':
|
||||||
if not data.get('drmProtected'):
|
if not data.get('drmProtected'):
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
video_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
video_url, video_id, 'mp4',
|
||||||
|
'm3u8' if is_live else 'm3u8_native',
|
||||||
m3u8_id=format_id, fatal=False))
|
m3u8_id=format_id, fatal=False))
|
||||||
elif ext == 'mpd':
|
elif ext == 'mpd':
|
||||||
formats.extend(self._extract_mpd_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
@@ -99,11 +106,6 @@ class TV2IE(InfoExtractor):
|
|||||||
raise ExtractorError('This video is DRM protected.', expected=True)
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
asset = self._download_json(
|
|
||||||
api_base + '.json', video_id,
|
|
||||||
'Downloading metadata JSON')['asset']
|
|
||||||
title = asset['title']
|
|
||||||
|
|
||||||
thumbnails = [{
|
thumbnails = [{
|
||||||
'id': thumbnail.get('@type'),
|
'id': thumbnail.get('@type'),
|
||||||
'url': thumbnail.get('url'),
|
'url': thumbnail.get('url'),
|
||||||
@@ -112,7 +114,7 @@ class TV2IE(InfoExtractor):
|
|||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'title': title,
|
'title': self._live_title(title) if is_live else title,
|
||||||
'description': strip_or_none(asset.get('description')),
|
'description': strip_or_none(asset.get('description')),
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'timestamp': parse_iso8601(asset.get('createTime')),
|
'timestamp': parse_iso8601(asset.get('createTime')),
|
||||||
@@ -120,6 +122,7 @@ class TV2IE(InfoExtractor):
|
|||||||
'view_count': int_or_none(asset.get('views')),
|
'view_count': int_or_none(asset.get('views')),
|
||||||
'categories': asset.get('keywords', '').split(','),
|
'categories': asset.get('keywords', '').split(','),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -168,13 +171,13 @@ class TV2ArticleIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class KatsomoIE(TV2IE):
|
class KatsomoIE(TV2IE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:katsomo|mtv)\.fi/(?:#!/)?(?:[^/]+/[0-9a-z-]+-\d+/[0-9a-z-]+-|[^/]+/\d+/[^/]+/)(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:katsomo|mtv(uutiset)?)\.fi/(?:sarja/[0-9a-z-]+-\d+/[0-9a-z-]+-|(?:#!/)?jakso/(?:\d+/[^/]+/)?|video/prog)(?P<id>\d+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://www.mtv.fi/sarja/mtv-uutiset-live-33001002003/lahden-pelicans-teki-kovan-ratkaisun-ville-nieminen-pihalle-1181321',
|
'url': 'https://www.mtv.fi/sarja/mtv-uutiset-live-33001002003/lahden-pelicans-teki-kovan-ratkaisun-ville-nieminen-pihalle-1181321',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1181321',
|
'id': '1181321',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'MTV Uutiset Live',
|
'title': 'Lahden Pelicans teki kovan ratkaisun – Ville Nieminen pihalle',
|
||||||
'description': 'Päätöksen teki Pelicansin hallitus.',
|
'description': 'Päätöksen teki Pelicansin hallitus.',
|
||||||
'timestamp': 1575116484,
|
'timestamp': 1575116484,
|
||||||
'upload_date': '20191130',
|
'upload_date': '20191130',
|
||||||
@@ -186,7 +189,60 @@ class KatsomoIE(TV2IE):
|
|||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://www.katsomo.fi/#!/jakso/33001005/studio55-fi/658521/jukka-kuoppamaki-tekee-yha-lauluja-vaikka-lentokoneessa',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.mtvuutiset.fi/video/prog1311159',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.katsomo.fi/#!/jakso/1311159',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
_API_DOMAIN = 'api.katsomo.fi'
|
_API_DOMAIN = 'api.katsomo.fi'
|
||||||
_PROTOCOLS = ('HLS', 'MPD')
|
_PROTOCOLS = ('HLS', 'MPD')
|
||||||
_GEO_COUNTRIES = ['FI']
|
_GEO_COUNTRIES = ['FI']
|
||||||
|
|
||||||
|
|
||||||
|
class MTVUutisetArticleIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)mtvuutiset\.fi/artikkeli/[^/]+/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.mtvuutiset.fi/artikkeli/tallaisia-vaurioita-viking-amorellassa-on-useamman-osaston-alla-vetta/7931384',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1311159',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Viking Amorellan matkustajien evakuointi on alkanut – tältä operaatio näyttää laivalla',
|
||||||
|
'description': 'Viking Amorellan matkustajien evakuointi on alkanut – tältä operaatio näyttää laivalla',
|
||||||
|
'timestamp': 1600608966,
|
||||||
|
'upload_date': '20200920',
|
||||||
|
'duration': 153.7886666,
|
||||||
|
'view_count': int,
|
||||||
|
'categories': list,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# multiple Youtube embeds
|
||||||
|
'url': 'https://www.mtvuutiset.fi/artikkeli/50-vuotta-subarun-vastaiskua/6070962',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
article_id = self._match_id(url)
|
||||||
|
article = self._download_json(
|
||||||
|
'http://api.mtvuutiset.fi/mtvuutiset/api/json/' + article_id,
|
||||||
|
article_id)
|
||||||
|
|
||||||
|
def entries():
|
||||||
|
for video in (article.get('videos') or []):
|
||||||
|
video_type = video.get('videotype')
|
||||||
|
video_url = video.get('url')
|
||||||
|
if not (video_url and video_type in ('katsomo', 'youtube')):
|
||||||
|
continue
|
||||||
|
yield self.url_result(
|
||||||
|
video_url, video_type.capitalize(), video.get('video_id'))
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries(), article_id, article.get('title'), article.get('description'))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TV4IE(InfoExtractor):
|
|||||||
tv4\.se/(?:[^/]+)/klipp/(?:.*)-|
|
tv4\.se/(?:[^/]+)/klipp/(?:.*)-|
|
||||||
tv4play\.se/
|
tv4play\.se/
|
||||||
(?:
|
(?:
|
||||||
(?:program|barn)/(?:[^/]+/|(?:[^\?]+)\?video_id=)|
|
(?:program|barn)/(?:(?:[^/]+/){1,2}|(?:[^\?]+)\?video_id=)|
|
||||||
iframe/video/|
|
iframe/video/|
|
||||||
film/|
|
film/|
|
||||||
sport/|
|
sport/|
|
||||||
@@ -65,6 +65,10 @@ class TV4IE(InfoExtractor):
|
|||||||
{
|
{
|
||||||
'url': 'http://www.tv4play.se/program/farang/3922081',
|
'url': 'http://www.tv4play.se/program/farang/3922081',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.tv4play.se/program/nyheterna/avsnitt/13315940',
|
||||||
|
'only_matching': True,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import int_or_none
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
str_or_none,
|
||||||
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VidioIE(InfoExtractor):
|
class VidioIE(InfoExtractor):
|
||||||
@@ -21,57 +27,63 @@ class VidioIE(InfoExtractor):
|
|||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'duration': 149,
|
'duration': 149,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
'uploader': 'TWELVE Pic',
|
||||||
|
'timestamp': 1444902800,
|
||||||
|
'upload_date': '20151015',
|
||||||
|
'uploader_id': 'twelvepictures',
|
||||||
|
'channel': 'Cover Music Video',
|
||||||
|
'channel_id': '280236',
|
||||||
|
'view_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': 'count:4',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.vidio.com/watch/77949-south-korea-test-fires-missile-that-can-strike-all-of-the-north',
|
'url': 'https://www.vidio.com/watch/77949-south-korea-test-fires-missile-that-can-strike-all-of-the-north',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._api_key = self._download_json(
|
||||||
|
'https://www.vidio.com/auth', None, data=b'')['api_key']
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
video_id, display_id = mobj.group('id', 'display_id')
|
data = self._download_json(
|
||||||
|
'https://api.vidio.com/videos/' + video_id, display_id, headers={
|
||||||
|
'Content-Type': 'application/vnd.api+json',
|
||||||
|
'X-API-KEY': self._api_key,
|
||||||
|
})
|
||||||
|
video = data['videos'][0]
|
||||||
|
title = video['title'].strip()
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage)
|
|
||||||
|
|
||||||
m3u8_url, duration, thumbnail = [None] * 3
|
|
||||||
|
|
||||||
clips = self._parse_json(
|
|
||||||
self._html_search_regex(
|
|
||||||
r'data-json-clips\s*=\s*(["\'])(?P<data>\[.+?\])\1',
|
|
||||||
webpage, 'video data', default='[]', group='data'),
|
|
||||||
display_id, fatal=False)
|
|
||||||
if clips:
|
|
||||||
clip = clips[0]
|
|
||||||
m3u8_url = clip.get('sources', [{}])[0].get('file')
|
|
||||||
duration = clip.get('clip_duration')
|
|
||||||
thumbnail = clip.get('image')
|
|
||||||
|
|
||||||
m3u8_url = m3u8_url or self._search_regex(
|
|
||||||
r'data(?:-vjs)?-clip-hls-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
|
|
||||||
webpage, 'hls url', group='url')
|
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
m3u8_url, display_id, 'mp4', entry_protocol='m3u8_native')
|
data['clips'][0]['hls_url'], display_id, 'mp4', 'm3u8_native')
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
duration = int_or_none(duration or self._search_regex(
|
get_first = lambda x: try_get(data, lambda y: y[x + 's'][0], dict) or {}
|
||||||
r'data-video-duration=(["\'])(?P<duration>\d+)\1', webpage,
|
channel = get_first('channel')
|
||||||
'duration', fatal=False, group='duration'))
|
user = get_first('user')
|
||||||
thumbnail = thumbnail or self._og_search_thumbnail(webpage)
|
username = user.get('username')
|
||||||
|
get_count = lambda x: int_or_none(video.get('total_' + x))
|
||||||
like_count = int_or_none(self._search_regex(
|
|
||||||
(r'<span[^>]+data-comment-vote-count=["\'](\d+)',
|
|
||||||
r'<span[^>]+class=["\'].*?\blike(?:__|-)count\b.*?["\'][^>]*>\s*(\d+)'),
|
|
||||||
webpage, 'like count', fatal=False))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._og_search_description(webpage),
|
'description': strip_or_none(video.get('description')),
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': video.get('image_url_medium'),
|
||||||
'duration': duration,
|
'duration': int_or_none(video.get('duration')),
|
||||||
'like_count': like_count,
|
'like_count': get_count('likes'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'uploader': user.get('name'),
|
||||||
|
'timestamp': parse_iso8601(video.get('created_at')),
|
||||||
|
'uploader_id': username,
|
||||||
|
'uploader_url': 'https://www.vidio.com/@' + username if username else None,
|
||||||
|
'channel': channel.get('name'),
|
||||||
|
'channel_id': str_or_none(channel.get('id')),
|
||||||
|
'view_count': get_count('view_count'),
|
||||||
|
'dislike_count': get_count('dislikes'),
|
||||||
|
'comment_count': get_count('comments'),
|
||||||
|
'tags': video.get('tag_list'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class VLiveIE(VLiveBaseIE):
|
|||||||
headers={'Referer': 'https://www.vlive.tv/'}, query=query)
|
headers={'Referer': 'https://www.vlive.tv/'}, query=query)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
||||||
self.raise_login_required(json.loads(e.cause.read().decode())['message'])
|
self.raise_login_required(json.loads(e.cause.read().decode('utf-8'))['message'])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VTMIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?vtm\.be/([^/?&#]+)~v(?P<id>[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://vtm.be/gast-vernielt-genkse-hotelkamer~ve7534523-279f-4b4d-a5c9-a33ffdbe23e1',
|
||||||
|
'md5': '37dca85fbc3a33f2de28ceb834b071f8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '192445',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Gast vernielt Genkse hotelkamer',
|
||||||
|
'timestamp': 1611060180,
|
||||||
|
'upload_date': '20210119',
|
||||||
|
'duration': 74,
|
||||||
|
# TODO: fix url _type result processing
|
||||||
|
# 'series': 'Op Interventie',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
uuid = self._match_id(url)
|
||||||
|
video = self._download_json(
|
||||||
|
'https://omc4vm23offuhaxx6hekxtzspi.appsync-api.eu-west-1.amazonaws.com/graphql',
|
||||||
|
uuid, query={
|
||||||
|
'query': '''{
|
||||||
|
getComponent(type: Video, uuid: "%s") {
|
||||||
|
... on Video {
|
||||||
|
description
|
||||||
|
duration
|
||||||
|
myChannelsVideo
|
||||||
|
program {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
publishedAt
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}''' % uuid,
|
||||||
|
}, headers={
|
||||||
|
'x-api-key': 'da2-lz2cab4tfnah3mve6wiye4n77e',
|
||||||
|
})['data']['getComponent']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': uuid,
|
||||||
|
'title': video.get('title'),
|
||||||
|
'url': 'http://mychannels.video/embed/%d' % video['myChannelsVideo'],
|
||||||
|
'description': video.get('description'),
|
||||||
|
'timestamp': parse_iso8601(video.get('publishedAt')),
|
||||||
|
'duration': int_or_none(video.get('duration')),
|
||||||
|
'series': try_get(video, lambda x: x['program']['title']),
|
||||||
|
'ie_key': 'Medialaan',
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from .youtube import YoutubeIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
@@ -47,6 +48,22 @@ class VVVVIDIE(InfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# video_type == 'video/youtube'
|
||||||
|
'url': 'https://www.vvvvid.it/show/404/one-punch-man/406/486683/trailer',
|
||||||
|
'md5': '33e0edfba720ad73a8782157fdebc648',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'RzmFKUDOUgw',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Trailer',
|
||||||
|
'upload_date': '20150906',
|
||||||
|
'description': 'md5:a5e802558d35247fee285875328c0b80',
|
||||||
|
'uploader_id': 'BandaiVisual',
|
||||||
|
'uploader': 'BANDAI NAMCO Arts Channel',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.vvvvid.it/show/434/perche-dovrei-guardarlo-di-dario-moccia/437/489048',
|
'url': 'https://www.vvvvid.it/show/434/perche-dovrei-guardarlo-di-dario-moccia/437/489048',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
@@ -154,12 +171,13 @@ class VVVVIDIE(InfoExtractor):
|
|||||||
if season_number:
|
if season_number:
|
||||||
info['season_number'] = int(season_number)
|
info['season_number'] = int(season_number)
|
||||||
|
|
||||||
for quality in ('_sd', ''):
|
video_type = video_data.get('video_type')
|
||||||
|
is_youtube = False
|
||||||
|
for quality in ('', '_sd'):
|
||||||
embed_code = video_data.get('embed_info' + quality)
|
embed_code = video_data.get('embed_info' + quality)
|
||||||
if not embed_code:
|
if not embed_code:
|
||||||
continue
|
continue
|
||||||
embed_code = ds(embed_code)
|
embed_code = ds(embed_code)
|
||||||
video_type = video_data.get('video_type')
|
|
||||||
if video_type in ('video/rcs', 'video/kenc'):
|
if video_type in ('video/rcs', 'video/kenc'):
|
||||||
if video_type == 'video/kenc':
|
if video_type == 'video/kenc':
|
||||||
kenc = self._download_json(
|
kenc = self._download_json(
|
||||||
@@ -172,19 +190,28 @@ class VVVVIDIE(InfoExtractor):
|
|||||||
if kenc_message:
|
if kenc_message:
|
||||||
embed_code += '?' + ds(kenc_message)
|
embed_code += '?' + ds(kenc_message)
|
||||||
formats.extend(self._extract_akamai_formats(embed_code, video_id))
|
formats.extend(self._extract_akamai_formats(embed_code, video_id))
|
||||||
|
elif video_type == 'video/youtube':
|
||||||
|
info.update({
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': YoutubeIE.ie_key(),
|
||||||
|
'url': embed_code,
|
||||||
|
})
|
||||||
|
is_youtube = True
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
formats.extend(self._extract_wowza_formats(
|
formats.extend(self._extract_wowza_formats(
|
||||||
'http://sb.top-ix.org/videomg/_definst_/mp4:%s/playlist.m3u8' % embed_code, video_id))
|
'http://sb.top-ix.org/videomg/_definst_/mp4:%s/playlist.m3u8' % embed_code, video_id))
|
||||||
metadata_from_url(embed_code)
|
metadata_from_url(embed_code)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
if not is_youtube:
|
||||||
|
self._sort_formats(formats)
|
||||||
|
info['formats'] = formats
|
||||||
|
|
||||||
metadata_from_url(video_data.get('thumbnail'))
|
metadata_from_url(video_data.get('thumbnail'))
|
||||||
info.update(self._extract_common_video_info(video_data))
|
info.update(self._extract_common_video_info(video_data))
|
||||||
info.update({
|
info.update({
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
|
||||||
'duration': int_or_none(video_data.get('length')),
|
'duration': int_or_none(video_data.get('length')),
|
||||||
'series': video_data.get('show_title'),
|
'series': video_data.get('show_title'),
|
||||||
'season_id': season_id,
|
'season_id': season_id,
|
||||||
|
|||||||
+573
-1214
File diff suppressed because it is too large
Load Diff
@@ -87,11 +87,16 @@ class ZypeIE(InfoExtractor):
|
|||||||
r'(["\'])(?P<url>(?:(?!\1).)+\.m3u8(?:(?!\1).)*)\1',
|
r'(["\'])(?P<url>(?:(?!\1).)+\.m3u8(?:(?!\1).)*)\1',
|
||||||
body, 'm3u8 url', group='url', default=None)
|
body, 'm3u8 url', group='url', default=None)
|
||||||
if not m3u8_url:
|
if not m3u8_url:
|
||||||
source = self._parse_json(self._search_regex(
|
source = self._search_regex(
|
||||||
r'(?s)sources\s*:\s*\[\s*({.+?})\s*\]', body,
|
r'(?s)sources\s*:\s*\[\s*({.+?})\s*\]', body, 'source')
|
||||||
'source'), video_id, js_to_json)
|
|
||||||
if source.get('integration') == 'verizon-media':
|
def get_attr(key):
|
||||||
m3u8_url = 'https://content.uplynk.com/%s.m3u8' % source['id']
|
return self._search_regex(
|
||||||
|
r'\b%s\s*:\s*([\'"])(?P<val>(?:(?!\1).)+)\1' % key,
|
||||||
|
source, key, group='val')
|
||||||
|
|
||||||
|
if get_attr('integration') == 'verizon-media':
|
||||||
|
m3u8_url = 'https://content.uplynk.com/%s.m3u8' % get_attr('id')
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
||||||
text_tracks = self._search_regex(
|
text_tracks = self._search_regex(
|
||||||
|
|||||||
+27
-8
@@ -16,6 +16,7 @@ from .compat import (
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
expand_path,
|
expand_path,
|
||||||
get_executable_path,
|
get_executable_path,
|
||||||
|
OUTTMPL_TYPES,
|
||||||
preferredencoding,
|
preferredencoding,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
@@ -831,19 +832,23 @@ def parseOpts(overrideArguments=None):
|
|||||||
metavar='TYPE:PATH', dest='paths', default={}, type='str',
|
metavar='TYPE:PATH', dest='paths', default={}, type='str',
|
||||||
action='callback', callback=_dict_from_multiple_values_options_callback,
|
action='callback', callback=_dict_from_multiple_values_options_callback,
|
||||||
callback_kwargs={
|
callback_kwargs={
|
||||||
'allowed_keys': 'home|temp|config|description|annotation|subtitle|infojson|thumbnail',
|
'allowed_keys': 'home|temp|%s' % '|'.join(OUTTMPL_TYPES.keys()),
|
||||||
'process': lambda x: x.strip()},
|
'process': lambda x: x.strip()},
|
||||||
help=(
|
help=(
|
||||||
'The paths where the files should be downloaded. '
|
'The paths where the files should be downloaded. '
|
||||||
'Specify the type of file and the path separated by a colon ":" '
|
'Specify the type of file and the path separated by a colon ":". '
|
||||||
'(supported: description|annotation|subtitle|infojson|thumbnail). '
|
'All the same types as --output are supported. '
|
||||||
'Additionally, you can also provide "home" and "temp" paths. '
|
'Additionally, you can also provide "home" and "temp" paths. '
|
||||||
'All intermediary files are first downloaded to the temp path and '
|
'All intermediary files are first downloaded to the temp path and '
|
||||||
'then the final files are moved over to the home path after download is finished. '
|
'then the final files are moved over to the home path after download is finished. '
|
||||||
'Note that this option is ignored if --output is an absolute path'))
|
'This option is ignored if --output is an absolute path'))
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'-o', '--output',
|
'-o', '--output',
|
||||||
dest='outtmpl', metavar='TEMPLATE',
|
metavar='[TYPE:]TEMPLATE', dest='outtmpl', default={}, type='str',
|
||||||
|
action='callback', callback=_dict_from_multiple_values_options_callback,
|
||||||
|
callback_kwargs={
|
||||||
|
'allowed_keys': '|'.join(OUTTMPL_TYPES.keys()),
|
||||||
|
'default_key': 'default', 'process': lambda x: x.strip()},
|
||||||
help='Output filename template, see "OUTPUT TEMPLATE" for details')
|
help='Output filename template, see "OUTPUT TEMPLATE" for details')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--output-na-placeholder',
|
'--output-na-placeholder',
|
||||||
@@ -892,11 +897,13 @@ def parseOpts(overrideArguments=None):
|
|||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'-c', '--continue',
|
'-c', '--continue',
|
||||||
action='store_true', dest='continue_dl', default=True,
|
action='store_true', dest='continue_dl', default=True,
|
||||||
help='Resume partially downloaded files (default)')
|
help='Resume partially downloaded files/fragments (default)')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--no-continue',
|
'--no-continue',
|
||||||
action='store_false', dest='continue_dl',
|
action='store_false', dest='continue_dl',
|
||||||
help='Restart download of partially downloaded files from beginning')
|
help=(
|
||||||
|
'Do not resume partially downloaded fragments. '
|
||||||
|
'If the file is unfragmented, restart download of the entire file'))
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--part',
|
'--part',
|
||||||
action='store_false', dest='nopart', default=False,
|
action='store_false', dest='nopart', default=False,
|
||||||
@@ -924,7 +931,7 @@ def parseOpts(overrideArguments=None):
|
|||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--write-info-json',
|
'--write-info-json',
|
||||||
action='store_true', dest='writeinfojson', default=False,
|
action='store_true', dest='writeinfojson', default=False,
|
||||||
help='Write video metadata to a .info.json file')
|
help='Write video metadata to a .info.json file (this may contain personal information)')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--no-write-info-json',
|
'--no-write-info-json',
|
||||||
action='store_false', dest='writeinfojson',
|
action='store_false', dest='writeinfojson',
|
||||||
@@ -937,6 +944,18 @@ def parseOpts(overrideArguments=None):
|
|||||||
'--no-write-annotations',
|
'--no-write-annotations',
|
||||||
action='store_false', dest='writeannotations',
|
action='store_false', dest='writeannotations',
|
||||||
help='Do not write video annotations (default)')
|
help='Do not write video annotations (default)')
|
||||||
|
filesystem.add_option(
|
||||||
|
'--write-playlist-metafiles',
|
||||||
|
action='store_true', dest='allow_playlist_files', default=True,
|
||||||
|
help=(
|
||||||
|
'Write playlist metadata in addition to the video metadata '
|
||||||
|
'when using --write-info-json, --write-description etc. (default)'))
|
||||||
|
filesystem.add_option(
|
||||||
|
'--no-write-playlist-metafiles',
|
||||||
|
action='store_false', dest='allow_playlist_files',
|
||||||
|
help=(
|
||||||
|
'Do not write playlist metadata when using '
|
||||||
|
'--write-info-json, --write-description etc.'))
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--get-comments',
|
'--get-comments',
|
||||||
action='store_true', dest='getcomments', default=False,
|
action='store_true', dest='getcomments', default=False,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
def run(self, info):
|
def run(self, info):
|
||||||
filename = info['filepath']
|
filename = info['filepath']
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
temp_filename = prepend_extension(filename, 'temp')
|
||||||
|
files_to_delete = []
|
||||||
|
|
||||||
if not info.get('thumbnails'):
|
if not info.get('thumbnails'):
|
||||||
self.to_screen('There aren\'t any thumbnails to embed')
|
self.to_screen('There aren\'t any thumbnails to embed')
|
||||||
@@ -78,7 +79,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
|
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
|
||||||
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
|
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
|
||||||
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
|
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
|
||||||
os.remove(encodeFilename(escaped_thumbnail_filename))
|
files_to_delete.append(escaped_thumbnail_filename)
|
||||||
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
|
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
|
||||||
# Rename back to unescaped for further processing
|
# Rename back to unescaped for further processing
|
||||||
os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename))
|
os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename))
|
||||||
@@ -154,6 +155,8 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
elif info['ext'] in ['ogg', 'opus']:
|
elif info['ext'] in ['ogg', 'opus']:
|
||||||
if not _has_mutagen:
|
if not _has_mutagen:
|
||||||
raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`')
|
raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`')
|
||||||
|
self.to_screen('Adding thumbnail to "%s"' % filename)
|
||||||
|
|
||||||
size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]'
|
size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]'
|
||||||
size_result = self.run_ffmpeg(thumbnail_filename, thumbnail_filename, ['-hide_banner'])
|
size_result = self.run_ffmpeg(thumbnail_filename, thumbnail_filename, ['-hide_banner'])
|
||||||
mobj = re.search(size_regex, size_result)
|
mobj = re.search(size_regex, size_result)
|
||||||
@@ -181,5 +184,9 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
if success and temp_filename != filename:
|
if success and temp_filename != filename:
|
||||||
os.remove(encodeFilename(filename))
|
os.remove(encodeFilename(filename))
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
||||||
files_to_delete = [] if self._already_have_thumbnail else [thumbnail_filename]
|
if self._already_have_thumbnail:
|
||||||
|
info['__files_to_move'][thumbnail_filename] = replace_extension(
|
||||||
|
info['__thumbnail_filename'], os.path.splitext(thumbnail_filename)[1][1:])
|
||||||
|
else:
|
||||||
|
files_to_delete.append(thumbnail_filename)
|
||||||
return files_to_delete, info
|
return files_to_delete, info
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class FFmpegPostProcessor(PostProcessor):
|
|||||||
|
|
||||||
def check_version(self):
|
def check_version(self):
|
||||||
if not self.available:
|
if not self.available:
|
||||||
raise FFmpegPostProcessorError('ffmpeg not found. Please install one.')
|
raise FFmpegPostProcessorError('ffmpeg not found. Please install')
|
||||||
|
|
||||||
required_version = '10-0' if self.basename == 'avconv' else '1.0'
|
required_version = '10-0' if self.basename == 'avconv' else '1.0'
|
||||||
if is_outdated_version(
|
if is_outdated_version(
|
||||||
@@ -165,7 +165,7 @@ class FFmpegPostProcessor(PostProcessor):
|
|||||||
|
|
||||||
def get_audio_codec(self, path):
|
def get_audio_codec(self, path):
|
||||||
if not self.probe_available and not self.available:
|
if not self.probe_available and not self.available:
|
||||||
raise PostProcessingError('ffprobe and ffmpeg not found. Please install one.')
|
raise PostProcessingError('ffprobe and ffmpeg not found. Please install')
|
||||||
try:
|
try:
|
||||||
if self.probe_available:
|
if self.probe_available:
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -578,7 +578,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
|||||||
in_filenames.append(metadata_filename)
|
in_filenames.append(metadata_filename)
|
||||||
options.extend(['-map_metadata', '1'])
|
options.extend(['-map_metadata', '1'])
|
||||||
|
|
||||||
if '__infojson_filepath' in info and info['ext'] in ('mkv', 'mka'):
|
if '__infojson_filename' in info and info['ext'] in ('mkv', 'mka'):
|
||||||
old_stream, new_stream = self.get_stream_number(
|
old_stream, new_stream = self.get_stream_number(
|
||||||
filename, ('tags', 'mimetype'), 'application/json')
|
filename, ('tags', 'mimetype'), 'application/json')
|
||||||
if old_stream is not None:
|
if old_stream is not None:
|
||||||
@@ -586,7 +586,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
|||||||
new_stream -= 1
|
new_stream -= 1
|
||||||
|
|
||||||
options.extend([
|
options.extend([
|
||||||
'-attach', info['__infojson_filepath'],
|
'-attach', info['__infojson_filename'],
|
||||||
'-metadata:s:%d' % new_stream, 'mimetype=application/json'
|
'-metadata:s:%d' % new_stream, 'mimetype=application/json'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import re
|
|||||||
|
|
||||||
from .common import PostProcessor
|
from .common import PostProcessor
|
||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
|
from ..utils import str_or_none
|
||||||
|
|
||||||
|
|
||||||
class MetadataFromFieldPP(PostProcessor):
|
class MetadataFromFieldPP(PostProcessor):
|
||||||
@@ -48,8 +49,12 @@ class MetadataFromFieldPP(PostProcessor):
|
|||||||
if field not in info:
|
if field not in info:
|
||||||
self.report_warning('Video doesnot have a %s' % field)
|
self.report_warning('Video doesnot have a %s' % field)
|
||||||
continue
|
continue
|
||||||
|
data_to_parse = str_or_none(info[field])
|
||||||
|
if data_to_parse is None:
|
||||||
|
self.report_warning('Field %s cannot be parsed' % field)
|
||||||
|
continue
|
||||||
self.write_debug('Searching for r"%s" in %s' % (regex, field))
|
self.write_debug('Searching for r"%s" in %s' % (regex, field))
|
||||||
match = re.search(regex, info[field])
|
match = re.search(regex, data_to_parse)
|
||||||
if match is None:
|
if match is None:
|
||||||
self.report_warning('Could not interpret video %s as "%s"' % (field, dictn['format']))
|
self.report_warning('Could not interpret video %s as "%s"' % (field, dictn['format']))
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class MoveFilesAfterDownloadPP(PostProcessor):
|
|||||||
dl_path, dl_name = os.path.split(encodeFilename(info['filepath']))
|
dl_path, dl_name = os.path.split(encodeFilename(info['filepath']))
|
||||||
finaldir = info.get('__finaldir', dl_path)
|
finaldir = info.get('__finaldir', dl_path)
|
||||||
finalpath = os.path.join(finaldir, dl_name)
|
finalpath = os.path.join(finaldir, dl_name)
|
||||||
|
self.files_to_move.update(info['__files_to_move'])
|
||||||
self.files_to_move[info['filepath']] = finalpath
|
self.files_to_move[info['filepath']] = finalpath
|
||||||
|
|
||||||
for oldfile, newfile in self.files_to_move.items():
|
for oldfile, newfile in self.files_to_move.items():
|
||||||
@@ -39,7 +40,7 @@ class MoveFilesAfterDownloadPP(PostProcessor):
|
|||||||
if os.path.exists(encodeFilename(newfile)):
|
if os.path.exists(encodeFilename(newfile)):
|
||||||
if self.get_param('overwrites', True):
|
if self.get_param('overwrites', True):
|
||||||
self.report_warning('Replacing existing file "%s"' % newfile)
|
self.report_warning('Replacing existing file "%s"' % newfile)
|
||||||
os.path.remove(encodeFilename(newfile))
|
os.remove(encodeFilename(newfile))
|
||||||
else:
|
else:
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
'Cannot move file "%s" out of temporary directory since "%s" already exists. '
|
'Cannot move file "%s" out of temporary directory since "%s" already exists. '
|
||||||
|
|||||||
+25
-1
@@ -50,6 +50,7 @@ from .compat import (
|
|||||||
compat_html_entities_html5,
|
compat_html_entities_html5,
|
||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_integer_types,
|
compat_integer_types,
|
||||||
|
compat_numeric_types,
|
||||||
compat_kwargs,
|
compat_kwargs,
|
||||||
compat_os_name,
|
compat_os_name,
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
@@ -3673,6 +3674,18 @@ def url_or_none(url):
|
|||||||
return url if re.match(r'^(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?):)?//', url) else None
|
return url if re.match(r'^(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?):)?//', url) else None
|
||||||
|
|
||||||
|
|
||||||
|
def strftime_or_none(timestamp, date_format, default=None):
|
||||||
|
datetime_object = None
|
||||||
|
try:
|
||||||
|
if isinstance(timestamp, compat_numeric_types): # unix timestamp
|
||||||
|
datetime_object = datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
elif isinstance(timestamp, compat_str): # assume YYYYMMDD
|
||||||
|
datetime_object = datetime.datetime.strptime(timestamp, '%Y%m%d')
|
||||||
|
return datetime_object.strftime(date_format)
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def parse_duration(s):
|
def parse_duration(s):
|
||||||
if not isinstance(s, compat_basestring):
|
if not isinstance(s, compat_basestring):
|
||||||
return None
|
return None
|
||||||
@@ -4156,7 +4169,18 @@ def qualities(quality_ids):
|
|||||||
return q
|
return q
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_OUTTMPL = '%(title)s [%(id)s].%(ext)s'
|
DEFAULT_OUTTMPL = {
|
||||||
|
'default': '%(title)s [%(id)s].%(ext)s',
|
||||||
|
}
|
||||||
|
OUTTMPL_TYPES = {
|
||||||
|
'subtitle': None,
|
||||||
|
'thumbnail': None,
|
||||||
|
'description': 'description',
|
||||||
|
'annotation': 'annotations.xml',
|
||||||
|
'infojson': 'info.json',
|
||||||
|
'pl_description': 'description',
|
||||||
|
'pl_infojson': 'info.json',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def limit_length(s, length):
|
def limit_length(s, length):
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '2021.01.24'
|
__version__ = '2021.01.29'
|
||||||
|
|||||||
Reference in New Issue
Block a user