Compare commits

..
Author SHA1 Message Date
pukkandan 3b3959630a Fix wrong user config (Closes #32)
:ci skip dl
2021-01-25 01:37:09 +05:30
39 changed files with 679 additions and 1390 deletions
+23 -26
View File
@@ -25,8 +25,8 @@ jobs:
run: sudo apt-get -y install zip pandoc man run: sudo apt-get -y install zip pandoc man
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: python scripts/update-version-workflow.py
- name: Print version - name: Check the output from My action
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}" run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
- name: Run Make - name: Run Make
run: make run: make
@@ -84,14 +84,11 @@ jobs:
with: with:
python-version: '3.8' python-version: '3.8'
- name: Install Requirements - name: Install Requirements
run: pip install pyinstaller mutagen run: pip install pyinstaller
- name: Bump version - name: Bump version
id: bump_version run: python scripts/update-version-workflow.py
run: python devscripts/update-version.py
- name: Print version
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
- name: Run PyInstaller Script - name: Run PyInstaller Script
run: python devscripts/pyinst.py 64 run: python pyinst.py
- name: Upload youtube-dlc.exe Windows binary - name: Upload youtube-dlc.exe Windows binary
id: upload-release-windows id: upload-release-windows
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -122,14 +119,11 @@ jobs:
python-version: '3.4.4' python-version: '3.4.4'
architecture: 'x86' architecture: 'x86'
- name: Install Requirements for 32 Bit - name: Install Requirements for 32 Bit
run: pip install pyinstaller==3.5 mutagen run: pip install pyinstaller==3.5
- name: Bump version - name: Bump version
id: bump_version run: python scripts/update-version-workflow.py
run: python devscripts/update-version.py
- name: Print version
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
- name: Run PyInstaller Script for 32 Bit - name: Run PyInstaller Script for 32 Bit
run: python devscripts/pyinst.py 32 run: python pyinst32.py
- name: Upload Executable youtube-dlc_x86.exe - name: Upload Executable youtube-dlc_x86.exe
id: upload-release-windows32 id: upload-release-windows32
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -168,15 +162,18 @@ jobs:
asset_name: SHA2-256SUMS asset_name: SHA2-256SUMS
asset_content_type: text/plain asset_content_type: text/plain
# update_version_badge: update_version_badge:
# runs-on: ubuntu-latest
# needs: build_unix runs-on: ubuntu-latest
# steps:
# - name: Create Version Badge needs: build_unix
# uses: schneegans/dynamic-badges-action@v1.0.0
# with: steps:
# auth: ${{ secrets.GIST_TOKEN }} - name: Create Version Badge
# gistID: c69cb23c3c5b3316248e52022790aa57 uses: schneegans/dynamic-badges-action@v1.0.0
# filename: version.json with:
# label: Version auth: ${{ secrets.GIST_TOKEN }}
# message: ${{ needs.build_unix.outputs.ytdlc_version }} gistID: c69cb23c3c5b3316248e52022790aa57
filename: version.json
label: Version
message: ${{ needs.build_unix.outputs.ytdlc_version }}
+39 -44
View File
@@ -1,43 +1,35 @@
# Python
*.pyc *.pyc
*.pyo *.pyo
*.class
*~
*.DS_Store
wine-py2exe/ wine-py2exe/
py2exe.log py2exe.log
*.kate-swp
build/ build/
dist/ dist/
zip/ zip/
tmp/
venv/
# Misc
*~
*.DS_Store
*.kate-swp
MANIFEST MANIFEST
test/local_parameters.json README.txt
youtube-dl.1
youtube-dlc.1
youtube-dl.bash-completion
youtube-dlc.bash-completion
youtube-dl.fish
youtube-dlc.fish
youtube_dl/extractor/lazy_extractors.py
youtube_dlc/extractor/lazy_extractors.py
youtube-dl
youtube-dlc
youtube-dl.exe
youtube-dlc.exe
youtube-dl.tar.gz
youtube-dlc.tar.gz
youtube-dlc.spec
.coverage .coverage
cover/ cover/
updates_key.pem updates_key.pem
*.egg-info *.egg-info
.tox
*.class
# Generated
README.txt
*.1
*.bash-completion
*.fish
*.exe
*.tar.gz
*.zsh
*.spec
# Binary
youtube-dl
youtube-dlc
*.exe
# Downloaded
*.srt *.srt
*.ttml *.ttml
*.sbv *.sbv
@@ -54,28 +46,31 @@ youtube-dlc
*.swf *.swf
*.part *.part
*.ytdl *.ytdl
*.swp
*.ogg
*.opus
# Config
*.conf *.conf
*.swp
*.spec *.spec
cookies *.exe
cookies.txt test/local_parameters.json
.tox
youtube-dl.zsh
youtube-dlc.zsh
# IntelliJ related files
# Text Editor / IDE
.idea .idea
*.iml *.iml
.vscode
*.sublime-workspace
*.sublime-project
!yt-dlp.sublime-project
# Lazy extractors tmp/
*/extractor/lazy_extractors.py venv/
# VS Code related files
.vscode
# SublimeText files
*.sublime-workspace
# Cookies
cookies
cookies.txt
# Plugins # Plugins
ytdlp_plugins/extractor/* ytdlp_plugins/extractor/*
+1 -35
View File
@@ -8,8 +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 and 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
* Update changelog in /releases * Update changelog in /releases
@@ -17,39 +16,6 @@
--> -->
### 2021.01.29
* **Features from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl)**: Co-authored by @animelover1984 and @bbepis
* Add `--get-comments`
* [youtube] Extract comments
* [billibilli] Added BiliBiliSearchIE, BilibiliChannelIE
* [billibilli] Extract comments
* [billibilli] Better video extraction
* Write playlist data to infojson
* [FFmpegMetadata] Embed infojson inside the video
* [EmbedThumbnail] Try embedding in mp4 using ffprobe and `-disposition`
* [EmbedThumbnail] Treat mka like mkv and mov like mp4
* [EmbedThumbnail] Embed in ogg/opus
* [VideoRemuxer] Conditionally remux video
* [VideoRemuxer] Add `-movflags +faststart` when remuxing to mp4
* [ffmpeg] Print entire stderr in verbose when there is error
* [EmbedSubtitle] Warn when embedding ass in mp4
* [anvato] Use NFLTokenGenerator if possible
* **Parse additional metadata**: New option `--parse-metadata` to extract additional metadata from existing fields
* The extracted fields can be used in `--output`
* Deprecated `--metadata-from-title`
* [Audius] Add extractor
* [youtube] Extract playlist description and write it to `.description` file
* Detect existing files even when using `recode`/`remux` (`extract-audio` is partially fixed)
* Fix wrong user config from v2021.01.24
* [youtube] Report error message from youtube as error instead of warning
* [FormatSort] Fix some fields not sorting from v2021.01.24
* [postprocessor] Deprecate `avconv`/`avprobe`. All current functionality is left untouched. But don't expect any new features to work with avconv
* [postprocessor] fix `write_debug` to not throw error when there is no `_downloader`
* [movefiles] Don't give "cant find" warning when move is unnecessary
* Refactor `update-version`, `pyinst.py` and related files
* [ffmpeg] Document more formats that are supported for remux/recode
### 2021.01.24 ### 2021.01.24
* **Merge youtube-dl:** Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16) * **Merge youtube-dl:** Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
* Plugin support ([documentation](https://github.com/pukkandan/yt-dlp#plugins)) * Plugin support ([documentation](https://github.com/pukkandan/yt-dlp#plugins))
+65 -73
View File
@@ -1,12 +1,9 @@
# YT-DLP # YT-DLP
[![Release version](https://img.shields.io/github/v/release/pukkandan/yt-dlp?color=brightgreen&label=Release)](https://github.com/pukkandan/yt-dlp/releases/latest) <!-- See: https://github.com/marketplace/actions/dynamic-badges -->
[![Release Version](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/pukkandan/c69cb23c3c5b3316248e52022790aa57/raw/version.json&color=brightgreen)](https://github.com/pukkandan/yt-dlp/releases/latest)
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE) [![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
[![CI Status](https://github.com/pukkandan/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/pukkandan/yt-dlp/actions) [![CI Status](https://github.com/pukkandan/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/pukkandan/yt-dlp/actions)
[![Commits](https://img.shields.io/github/commit-activity/m/pukkandan/yt-dlp?label=commits)](https://github.com/pukkandan/yt-dlp/commits)
[![Last Commit](https://img.shields.io/github/last-commit/pukkandan/yt-dlp/master)](https://github.com/pukkandan/yt-dlp/commits)
[![Downloads](https://img.shields.io/github/downloads/pukkandan/yt-dlp/total)](https://github.com/pukkandan/yt-dlp/releases/latest)
[![PyPi Downloads](https://img.shields.io/pypi/dm/yt-dlp?label=PyPi)](https://pypi.org/project/yt-dlp)
A command-line program to download videos from youtube.com and many other [video platforms](docs/supportedsites.md) A command-line program to download videos from youtube.com and many other [video platforms](docs/supportedsites.md)
@@ -56,14 +53,12 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* **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.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 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.
* **Youtube improvements**: * **Youtube improvements**:
* All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works correctly and support downloading multiple pages of content * All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works correctly and support downloading multiple pages of content
* Youtube search works correctly (`ytsearch:`, `ytsearchdate:`) along with Search URLs * Youtube search works correctly (`ytsearch:`, `ytsearchdate:`) along with Search URLs
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour * Redirect channel's home URL automatically to `/video` to preserve the old behaviour
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius * **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina * **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina
@@ -75,11 +70,10 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details * **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
* **Other new options**: `--parse-metadata`, `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc * **Other new options**: `--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`, `%(duration_string)s` 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
@@ -91,7 +85,7 @@ If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the
# INSTALLATION # INSTALLATION
You can install yt-dlp using one of the following methods: You can install yt-dlp using one of the following methods:
* Use [PyPI package](https://pypi.org/project/yt-dlp): `python -m pip install --upgrade yt-dlp` * Use [PyPI package](https://pypi.org/project/yt-dlp/): `python -m pip install --upgrade yt-dlp`
* Download the binary from the [latest release](https://github.com/pukkandan/yt-dlp/releases/latest) * Download the binary from the [latest release](https://github.com/pukkandan/yt-dlp/releases/latest)
* Use pip+git: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp.git@release` * Use pip+git: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp.git@release`
* Install master branch: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp` * Install master branch: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp`
@@ -102,15 +96,16 @@ You can install yt-dlp using one of the following methods:
### COMPILE ### COMPILE
**For Windows**: **For Windows**:
To build the Windows executable, you must have pyinstaller (and optionally mutagen for embedding thumbnail in opus/ogg files) To build the Windows executable yourself (without version info!)
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)
You can also build the executable without any version info or metadata by using:
python -m pip install --upgrade pyinstaller
pyinstaller.exe youtube_dlc\__main__.py --onefile --name youtube-dlc pyinstaller.exe youtube_dlc\__main__.py --onefile --name youtube-dlc
Or simply execute the `make_win.bat` if pyinstaller is installed.
There will be a `youtube-dlc.exe` in `/dist`
New way to build Windows is to use `python pyinst.py` (please use python3 64Bit)
For 32Bit Version use a 32Bit Version of python (3 preferred here as well) and run `python pyinst32.py`
**For Unix**: **For Unix**:
You will need the required build tools You will need the required build tools
@@ -119,7 +114,6 @@ Then simply type this
make make
**Note**: In either platform, `devscripts\update-version.py` can be used to automatically update the version number
# DESCRIPTION # DESCRIPTION
**youtube-dlc** is a command-line program to download videos from youtube.com many other [video platforms](docs/supportedsites.md). It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on macOS. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. **youtube-dlc** is a command-line program to download videos from youtube.com many other [video platforms](docs/supportedsites.md). It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on macOS. It is released to the public domain, which means you can modify it, redistribute it or use it however you like.
@@ -323,8 +317,8 @@ Then simply type this
--downloader-args NAME:ARGS Give these arguments to the external --downloader-args NAME:ARGS Give these arguments to the external
downloader. Specify the downloader name and downloader. Specify the downloader name and
the arguments separated by a colon ":". You the arguments separated by a colon ":". You
can use this option multiple times can use this option multiple times (Alias:
(Alias: --external-downloader-args) --external-downloader-args)
## Filesystem Options: ## Filesystem Options:
-a, --batch-file FILE File containing URLs to download ('-' for -a, --batch-file FILE File containing URLs to download ('-' for
@@ -378,8 +372,6 @@ Then simply type this
--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)
--get-comments Retrieve video comments to be placed in the
.info.json file
--load-info-json FILE JSON file containing the video information --load-info-json FILE JSON file containing the video information
(created with the "--write-info-json" (created with the "--write-info-json"
option) option)
@@ -514,17 +506,17 @@ Then simply type this
--list-formats-old Present the output of -F in the old form --list-formats-old Present the output of -F in the old form
(Alias: --no-list-formats-as-table) (Alias: --no-list-formats-as-table)
--youtube-include-dash-manifest Download the DASH manifests and related --youtube-include-dash-manifest Download the DASH manifests and related
data on YouTube videos (default) data on YouTube videos (default) (Alias:
(Alias: --no-youtube-skip-dash-manifest) --no-youtube-skip-dash-manifest)
--youtube-skip-dash-manifest Do not download the DASH manifests and --youtube-skip-dash-manifest Do not download the DASH manifests and
related data on YouTube videos related data on YouTube videos (Alias:
(Alias: --no-youtube-include-dash-manifest) --no-youtube-include-dash-manifest)
--youtube-include-hls-manifest Download the HLS manifests and related data --youtube-include-hls-manifest Download the HLS manifests and related data
on YouTube videos (default) on YouTube videos (default) (Alias:
(Alias: --no-youtube-skip-hls-manifest) --no-youtube-skip-hls-manifest)
--youtube-skip-hls-manifest Do not download the HLS manifests and --youtube-skip-hls-manifest Do not download the HLS manifests and
related data on YouTube videos related data on YouTube videos (Alias:
(Alias: --no-youtube-include-hls-manifest) --no-youtube-include-hls-manifest)
--merge-output-format FORMAT If a merge is required (e.g. --merge-output-format FORMAT If a merge is required (e.g.
bestvideo+bestaudio), output to given bestvideo+bestaudio), output to given
container format. One of mkv, mp4, ogg, container format. One of mkv, mp4, ogg,
@@ -568,26 +560,23 @@ Then simply type this
## Post-Processing Options: ## Post-Processing Options:
-x, --extract-audio Convert video files to audio-only files -x, --extract-audio Convert video files to audio-only files
(requires ffmpeg and ffprobe) (requires ffmpeg or avconv and ffprobe or
avprobe)
--audio-format FORMAT Specify audio format: "best", "aac", --audio-format FORMAT Specify audio format: "best", "aac",
"flac", "mp3", "m4a", "opus", "vorbis", or "flac", "mp3", "m4a", "opus", "vorbis", or
"wav"; "best" by default; No effect without "wav"; "best" by default; No effect without
-x -x
--audio-quality QUALITY Specify ffmpeg audio quality, insert a --audio-quality QUALITY Specify ffmpeg/avconv audio quality, insert
value between 0 (better) and 9 (worse) for a value between 0 (better) and 9 (worse)
VBR or a specific bitrate like 128K for VBR or a specific bitrate like 128K
(default 5) (default 5)
--remux-video FORMAT Remux the video into another container if --remux-video FORMAT Remux the video into another container if
necessary (currently supported: mp4|mkv|flv necessary (currently supported: mp4|mkv).
|webm|mov|avi|mp3|mka|m4a|ogg|opus). If If target container does not support the
target container does not support the video/audio codec, remuxing will fail
video/audio codec, remuxing will fail. You
can specify multiple rules; eg.
"aac>m4a/mov>mp4/mkv" will remux aac to
m4a, mov to mp4 and anything else to mkv.
--recode-video FORMAT Re-encode the video into another format if --recode-video FORMAT Re-encode the video into another format if
re-encoding is necessary. The supported re-encoding is necessary (currently
formats are the same as --remux-video supported: mp4|flv|ogg|webm|mkv|avi)
--postprocessor-args NAME:ARGS Give these arguments to the postprocessors. --postprocessor-args NAME:ARGS Give these arguments to the postprocessors.
Specify the postprocessor/executable name Specify the postprocessor/executable name
and the arguments separated by a colon ":" and the arguments separated by a colon ":"
@@ -599,14 +588,15 @@ Then simply type this
FixupStretched, FixupM4a, FixupM3u8, FixupStretched, FixupM4a, FixupM3u8,
SubtitlesConvertor and EmbedThumbnail. The SubtitlesConvertor and EmbedThumbnail. The
supported executables are: SponSkrub, supported executables are: SponSkrub,
FFmpeg, FFprobe, and AtomicParsley. You can FFmpeg, FFprobe, avconf, avprobe and
use this option multiple times to give AtomicParsley. You can use this option
different arguments to different multiple times to give different arguments
postprocessors. You can also specify to different postprocessors. You can also
"PP+EXE:ARGS" to give the arguments to the specify "PP+EXE:ARGS" to give the arguments
specified executable only when being used to the specified executable only when being
by the specified postprocessor. You can use used by the specified postprocessor. You
this option multiple times (Alias: --ppa) can use this option multiple times (Alias:
--ppa)
-k, --keep-video Keep the intermediate video file on disk -k, --keep-video Keep the intermediate video file on disk
after post-processing after post-processing
--no-keep-video Delete the intermediate video file after --no-keep-video Delete the intermediate video file after
@@ -620,20 +610,16 @@ Then simply type this
--no-embed-thumbnail Do not embed thumbnail (default) --no-embed-thumbnail Do not embed thumbnail (default)
--add-metadata Write metadata to the video file --add-metadata Write metadata to the video file
--no-add-metadata Do not write metadata (default) --no-add-metadata Do not write metadata (default)
--parse-metadata FIELD:FORMAT Parse additional metadata like title/artist --metadata-from-title FORMAT Parse additional metadata like song title /
from other fields. Give field name to artist from the video title. The format
extract data from, and format of the field syntax is the same as --output. Regular
seperated by a ":". Either regular expression with named capture groups may
expression with named capture groups or a
similar syntax to the output template can
also be used. The parsed parameters replace also be used. The parsed parameters replace
any existing values and can be use in existing values. Example: --metadata-from-
output templateThis option can be used title "%(artist)s - %(title)s" matches a
multiple times. Example: --parse-metadata
"title:%(artist)s - %(title)s" matches a
title like "Coldplay - Paradise". Example title like "Coldplay - Paradise". Example
(regex): --parse-metadata (regex): --metadata-from-title
"description:Artist - (?P<artist>.+?)" "(?P<artist>.+?) - (?P<title>.+)"
--xattrs Write metadata to the video file's xattrs --xattrs Write metadata to the video file's xattrs
(using dublin core and xdg standards) (using dublin core and xdg standards)
--fixup POLICY Automatically correct known faults of the --fixup POLICY Automatically correct known faults of the
@@ -641,9 +627,15 @@ Then simply type this
emit a warning), detect_or_warn (the emit a warning), detect_or_warn (the
default; fix file if we can, warn default; fix file if we can, warn
otherwise) otherwise)
--ffmpeg-location PATH Location of the ffmpeg binary; either the --prefer-avconv Prefer avconv over ffmpeg for running the
path to the binary or its containing postprocessors (Alias: --no-prefer-ffmpeg)
directory --prefer-ffmpeg Prefer ffmpeg over avconv for running the
postprocessors (default)
(Alias: --no-prefer-avconv)
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
either the path to the binary or its
containing directory
(Alias: --avconv-location)
--exec CMD Execute a command on the file after --exec CMD Execute a command on the file after
downloading and post-processing, similar to downloading and post-processing, similar to
find's -exec syntax. Example: --exec 'adb find's -exec syntax. Example: --exec 'adb
@@ -776,7 +768,7 @@ The basic usage is not to set any template arguments when downloading a single f
- `is_live` (boolean): Whether this video is a live stream or a fixed-length video - `is_live` (boolean): Whether this video is a live stream or a fixed-length video
- `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL - `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL
- `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL - `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL
- `format` (string): A human-readable description of the format - `format` (string): A human-readable description of the format
- `format_id` (string): Format code specified by `--format` - `format_id` (string): Format code specified by `--format`
- `format_note` (string): Additional info about the format - `format_note` (string): Additional info about the format
- `width` (numeric): Width of the video - `width` (numeric): Width of the video
@@ -881,7 +873,7 @@ $ youtube-dlc -o - BaW_jenozKc
# FORMAT SELECTION # FORMAT SELECTION
By default, youtube-dlc tries to download the best available quality if you **don't** pass any options. By default, youtube-dlc tries to download the best available quality if you **don't** pass any options.
This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg is unavailable, or if you use youtube-dlc to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`. This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg and avconv are unavailable, or if you use youtube-dlc to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`.
The general syntax for format selection is `--f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download. The general syntax for format selection is `--f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
@@ -912,7 +904,7 @@ If you want to download multiple videos and they don't have the same formats ava
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`. If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg. If `--no-video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, if `--no-audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`. You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg or avconv installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg/avconv. If `--no-video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, if `--no-audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
## Filtering Formats ## Filtering Formats
@@ -1106,7 +1098,7 @@ $ youtube-dlc -S '+res:480,codec,br'
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example. Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/youtube-dlc`), or the root directory of the module if you are running directly from source-code (`<root dir>/youtube_dlc/__main__.py`) **Note**: `<root-dir>` is the directory of the binary (`<root-dir>/youtube-dlc`), or the root directory of the module if you are running directly from source-code ((`<root dir>/youtube_dlc/__main__.py`)
# MORE # MORE
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq) For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl)
-69
View File
@@ -1,69 +0,0 @@
from __future__ import unicode_literals
import sys
from PyInstaller.utils.win32.versioninfo import (
VarStruct, VarFileInfo, StringStruct, StringTable,
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
)
import PyInstaller.__main__
assert len(sys.argv) > 1 and sys.argv[1] in ("32", "64")
_x86 = "_x86" if sys.argv[1] == "32" else ""
FILE_DESCRIPTION = 'Media Downloader%s' % (" (32 Bit)" if _x86 else '')
SHORT_URLS = {"32": "git.io/JUGsM", "64": "git.io/JLh7K"}
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
VERSION = locals()['__version__']
VERSION_LIST = VERSION.replace('-', '.').split('.')
VERSION_LIST = list(map(int, VERSION_LIST)) + [0] * (4 - len(VERSION_LIST))
print('Version: %s%s' % (VERSION, _x86))
print('Remember to update the version using devscipts\\update-version.py')
VERSION_FILE = VSVersionInfo(
ffi=FixedFileInfo(
filevers=VERSION_LIST,
prodvers=VERSION_LIST,
mask=0x3F,
flags=0x0,
OS=0x4,
fileType=0x1,
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo([
StringTable(
"040904B0", [
StringStruct("Comments", "Youtube-dlc%s Command Line Interface." % _x86),
StringStruct("CompanyName", "pukkandan@gmail.com"),
StringStruct("FileDescription", FILE_DESCRIPTION),
StringStruct("FileVersion", VERSION),
StringStruct("InternalName", "youtube-dlc%s" % _x86),
StringStruct(
"LegalCopyright",
"pukkandan@gmail.com | UNLICENSE",
),
StringStruct("OriginalFilename", "youtube-dlc%s.exe" % _x86),
StringStruct("ProductName", "Youtube-dlc%s" % _x86),
StringStruct("ProductVersion", "%s%s | %s" % (VERSION, _x86, SHORT_URLS[sys.argv[1]])),
])]),
VarFileInfo([VarStruct("Translation", [0, 1200])])
]
)
PyInstaller.__main__.run([
'--name=youtube-dlc%s' % _x86,
'--onefile',
'--icon=devscripts/cloud.ico',
'--exclude-module=youtube_dl',
'--exclude-module=test',
'--exclude-module=ytdlp_plugins',
'--hidden-import=mutagen',
'youtube_dlc/__main__.py',
])
SetVersion('dist/youtube-dlc%s.exe' % _x86, VERSION_FILE)
-31
View File
@@ -1,31 +0,0 @@
from __future__ import unicode_literals
from datetime import datetime
# import urllib.request
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
# old_version = response.read().decode('utf-8')
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
old_version = locals()['__version__']
old_version_list = old_version.replace('-', '.').split(".", 4)
old_ver = '.'.join(old_version_list[:3])
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
ver = datetime.now().strftime("%Y.%m.%d")
rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
VERSION = '.'.join((ver, rev)) if rev else ver
# VERSION_LIST = [(int(v) for v in ver.split(".") + [rev or 0])]
print('::set-output name=ytdlc_version::' + VERSION)
file_version_py = open('youtube_dlc/version.py', 'rt')
data = file_version_py.read()
data = data.replace(old_version, VERSION)
file_version_py.close()
file_version_py = open('youtube_dlc/version.py', 'wt')
file_version_py.write(data)
file_version_py.close()
-4
View File
@@ -80,8 +80,6 @@
- **AudioBoom** - **AudioBoom**
- **audiomack** - **audiomack**
- **audiomack:album** - **audiomack:album**
- **Audius**
- **audius:playlist**
- **AWAAN** - **AWAAN**
- **awaan:live** - **awaan:live**
- **awaan:season** - **awaan:season**
@@ -114,9 +112,7 @@
- **BiliBili** - **BiliBili**
- **BilibiliAudio** - **BilibiliAudio**
- **BilibiliAudioAlbum** - **BilibiliAudioAlbum**
- **BilibiliChannel**
- **BiliBiliPlayer** - **BiliBiliPlayer**
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
- **BioBioChileTV** - **BioBioChileTV**
- **Biography** - **Biography**
- **BIQLE** - **BIQLE**
+1
View File
@@ -0,0 +1 @@
py -m PyInstaller youtube_dlc\__main__.py --onefile --name youtube-dlc --version-file win\ver.txt --icon win\icon\cloud.ico --upx-exclude=vcruntime140.dll --exclude-module ytdlp_plugins
+92
View File
@@ -0,0 +1,92 @@
from __future__ import unicode_literals
from PyInstaller.utils.win32.versioninfo import (
VarStruct, VarFileInfo, StringStruct, StringTable,
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
)
import PyInstaller.__main__
from datetime import datetime
FILE_DESCRIPTION = 'Media Downloader'
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
_LATEST_VERSION = locals()['__version__']
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
if len(_OLD_VERSION) > 0:
old_ver = _OLD_VERSION[0]
old_rev = ''
if len(_OLD_VERSION) > 1:
old_rev = _OLD_VERSION[1]
now = datetime.now()
# ver = f'{datetime.today():%Y.%m.%d}'
ver = now.strftime("%Y.%m.%d")
rev = ''
if old_ver == ver:
if old_rev:
rev = int(old_rev) + 1
else:
rev = 1
_SEPARATOR = '-'
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
print(version)
version_list = ver.split(".")
_year, _month, _day = [int(value) for value in version_list]
_rev = 0
if rev:
_rev = rev
_ver_tuple = _year, _month, _day, _rev
version_file = VSVersionInfo(
ffi=FixedFileInfo(
filevers=_ver_tuple,
prodvers=_ver_tuple,
mask=0x3F,
flags=0x0,
OS=0x4,
fileType=0x1,
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo(
[
StringTable(
"040904B0",
[
StringStruct("Comments", "Youtube-dlc Command Line Interface."),
StringStruct("CompanyName", "theidel@uni-bremen.de"),
StringStruct("FileDescription", FILE_DESCRIPTION),
StringStruct("FileVersion", version),
StringStruct("InternalName", "youtube-dlc"),
StringStruct(
"LegalCopyright",
"theidel@uni-bremen.de | UNLICENSE",
),
StringStruct("OriginalFilename", "youtube-dlc.exe"),
StringStruct("ProductName", "Youtube-dlc"),
StringStruct("ProductVersion", version + " | git.io/JLh7K"),
],
)
]
),
VarFileInfo([VarStruct("Translation", [0, 1200])])
]
)
PyInstaller.__main__.run([
'--name=youtube-dlc',
'--onefile',
'--icon=win/icon/cloud.ico',
'youtube_dlc/__main__.py',
])
SetVersion('dist/youtube-dlc.exe', version_file)
+92
View File
@@ -0,0 +1,92 @@
from __future__ import unicode_literals
from PyInstaller.utils.win32.versioninfo import (
VarStruct, VarFileInfo, StringStruct, StringTable,
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
)
import PyInstaller.__main__
from datetime import datetime
FILE_DESCRIPTION = 'Media Downloader 32 Bit Version'
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
_LATEST_VERSION = locals()['__version__']
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
if len(_OLD_VERSION) > 0:
old_ver = _OLD_VERSION[0]
old_rev = ''
if len(_OLD_VERSION) > 1:
old_rev = _OLD_VERSION[1]
now = datetime.now()
# ver = f'{datetime.today():%Y.%m.%d}'
ver = now.strftime("%Y.%m.%d")
rev = ''
if old_ver == ver:
if old_rev:
rev = int(old_rev) + 1
else:
rev = 1
_SEPARATOR = '-'
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
print(version)
version_list = ver.split(".")
_year, _month, _day = [int(value) for value in version_list]
_rev = 0
if rev:
_rev = rev
_ver_tuple = _year, _month, _day, _rev
version_file = VSVersionInfo(
ffi=FixedFileInfo(
filevers=_ver_tuple,
prodvers=_ver_tuple,
mask=0x3F,
flags=0x0,
OS=0x4,
fileType=0x1,
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo(
[
StringTable(
"040904B0",
[
StringStruct("Comments", "Youtube-dlc_x86 Command Line Interface."),
StringStruct("CompanyName", "theidel@uni-bremen.de"),
StringStruct("FileDescription", FILE_DESCRIPTION),
StringStruct("FileVersion", version),
StringStruct("InternalName", "youtube-dlc_x86"),
StringStruct(
"LegalCopyright",
"theidel@uni-bremen.de | UNLICENSE",
),
StringStruct("OriginalFilename", "youtube-dlc_x86.exe"),
StringStruct("ProductName", "Youtube-dlc_x86"),
StringStruct("ProductVersion", version + "_x86 | git.io/JUGsM"),
],
)
]
),
VarFileInfo([VarStruct("Translation", [0, 1200])])
]
)
PyInstaller.__main__.run([
'--name=youtube-dlc_x86',
'--onefile',
'--icon=win/icon/cloud.ico',
'youtube_dlc/__main__.py',
])
SetVersion('dist/youtube-dlc_x86.exe', version_file)
-1
View File
@@ -1 +0,0 @@
mutagen
+44
View File
@@ -0,0 +1,44 @@
from __future__ import unicode_literals
from datetime import datetime
# import urllib.request
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
# _LATEST_VERSION = response.read().decode('utf-8')
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
_LATEST_VERSION = locals()['__version__']
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
if len(_OLD_VERSION) > 0:
old_ver = _OLD_VERSION[0]
old_rev = ''
if len(_OLD_VERSION) > 1:
old_rev = _OLD_VERSION[1]
now = datetime.now()
# ver = f'{datetime.today():%Y.%m.%d}'
ver = now.strftime("%Y.%m.%d")
rev = ''
if old_ver == ver:
if old_rev:
rev = int(old_rev) + 1
else:
rev = 1
_SEPARATOR = '-'
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
print('::set-output name=ytdlc_version::' + version)
file_version_py = open('youtube_dlc/version.py', 'rt')
data = file_version_py.read()
data = data.replace(locals()['__version__'], version)
file_version_py.close()
file_version_py = open('youtube_dlc/version.py', 'wt')
file_version_py.write(data)
file_version_py.close()
+33
View File
@@ -0,0 +1,33 @@
# Unused
from __future__ import unicode_literals
from datetime import datetime
import urllib.request
response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
_LATEST_VERSION = response.read().decode('utf-8')
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
if len(_OLD_VERSION) > 0:
old_ver = _OLD_VERSION[0]
old_rev = ''
if len(_OLD_VERSION) > 1:
old_rev = _OLD_VERSION[1]
now = datetime.now()
# ver = f'{datetime.today():%Y.%m.%d}'
ver = now.strftime("%Y.%m.%d")
rev = ''
if old_ver == ver:
if old_rev:
rev = int(old_rev) + 1
else:
rev = 1
_SEPARATOR = '-'
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
+1 -1
View File
@@ -2,5 +2,5 @@
universal = True universal = True
[flake8] [flake8]
exclude = youtube_dlc/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv,devscripts/create-github-release.py,devscripts/release.sh,devscripts/show-downloads-statistics.py exclude = youtube_dlc/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv,devscripts/create-github-release.py,devscripts/release.sh,devscripts/show-downloads-statistics.py,scripts/update-version.py
ignore = E402,E501,E731,E741,W503 ignore = E402,E501,E731,E741,W503
+4 -11
View File
@@ -7,12 +7,10 @@ import warnings
import sys import sys
from distutils.spawn import spawn from distutils.spawn import spawn
# Get the version from youtube_dlc/version.py without importing the package # Get the version from youtube_dlc/version.py without importing the package
exec(compile(open('youtube_dlc/version.py').read(), exec(compile(open('youtube_dlc/version.py').read(),
'youtube_dlc/version.py', 'exec')) 'youtube_dlc/version.py', 'exec'))
DESCRIPTION = 'Command-line program to download videos from YouTube.com and many other other video platforms.' DESCRIPTION = 'Command-line program to download videos from YouTube.com and many other other video platforms.'
LONG_DESCRIPTION = '\n\n'.join(( LONG_DESCRIPTION = '\n\n'.join((
@@ -20,9 +18,6 @@ LONG_DESCRIPTION = '\n\n'.join((
'**PS**: Many links in this document will not work since this is a copy of the README.md from Github', '**PS**: Many links in this document will not work since this is a copy of the README.md from Github',
open("README.md", "r", encoding="utf-8").read())) open("README.md", "r", encoding="utf-8").read()))
REQUIREMENTS = ['mutagen']
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe': if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
print("inv") print("inv")
else: else:
@@ -46,8 +41,10 @@ else:
params = { params = {
'data_files': data_files, 'data_files': data_files,
} }
#if setuptools_available:
params['entry_points'] = {'console_scripts': ['youtube-dlc = youtube_dlc:main']} params['entry_points'] = {'console_scripts': ['youtube-dlc = youtube_dlc:main']}
#else:
# params['scripts'] = ['bin/youtube-dlc']
class build_lazy_extractors(Command): class build_lazy_extractors(Command):
description = 'Build the extractor lazy loading module' description = 'Build the extractor lazy loading module'
@@ -65,9 +62,6 @@ class build_lazy_extractors(Command):
dry_run=self.dry_run, dry_run=self.dry_run,
) )
packages = find_packages(exclude=("youtube_dl", "test", "ytdlp_plugins"))
setup( setup(
name="yt-dlp", name="yt-dlp",
version=__version__, version=__version__,
@@ -77,8 +71,7 @@ setup(
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://github.com/pukkandan/yt-dlp", url="https://github.com/pukkandan/yt-dlp",
packages=packages, packages=find_packages(exclude=("youtube_dl","test",)),
install_requires=REQUIREMENTS,
project_urls={ project_urls={
'Documentation': 'https://github.com/pukkandan/yt-dlp#yt-dlp', 'Documentation': 'https://github.com/pukkandan/yt-dlp#yt-dlp',
'Source': 'https://github.com/pukkandan/yt-dlp', 'Source': 'https://github.com/pukkandan/yt-dlp',
+2 -8
View File
@@ -8,16 +8,10 @@ import sys
import unittest import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from youtube_dlc.postprocessor import MetadataFromFieldPP, MetadataFromTitlePP from youtube_dlc.postprocessor import MetadataFromTitlePP
class TestMetadataFromField(unittest.TestCase):
def test_format_to_regex(self):
pp = MetadataFromFieldPP(None, ['title:%(title)s - %(artist)s'])
self.assertEqual(pp._data[0]['regex'], r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)')
class TestMetadataFromTitle(unittest.TestCase): class TestMetadataFromTitle(unittest.TestCase):
def test_format_to_regex(self): def test_format_to_regex(self):
pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s') pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s')
self.assertEqual(pp._titleregex, r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)') self.assertEqual(pp._titleregex, r'(?P<title>.+)\ \-\ (?P<artist>.+)')
+2
View File
@@ -15,6 +15,8 @@ IGNORED_FILES = [
'setup.py', # http://bugs.python.org/issue13943 'setup.py', # http://bugs.python.org/issue13943
'conf.py', 'conf.py',
'buildserver.py', 'buildserver.py',
'pyinst.py',
'pyinst32.py',
] ]
IGNORED_DIRS = [ IGNORED_DIRS = [

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+45
View File
@@ -0,0 +1,45 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(16, 9, 2020, 0),
prodvers=(16, 9, 2020, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
# OS=0x40004,
OS=0x4,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'040904B0',
[StringStruct(u'Comments', u'Youtube-dlc Command Line Interface.'),
StringStruct(u'CompanyName', u'theidel@uni-bremen.de'),
StringStruct(u'FileDescription', u'Media Downloader'),
StringStruct(u'FileVersion', u'16.9.2020.0'),
StringStruct(u'InternalName', u'youtube-dlc'),
StringStruct(u'LegalCopyright', u'theidel@uni-bremen.de | UNLICENSE'),
StringStruct(u'OriginalFilename', u'youtube-dlc.exe'),
StringStruct(u'ProductName', u'Youtube-dlc'),
StringStruct(u'ProductVersion', u'16.9.2020.0 | git.io/JUGsM')])
]),
VarFileInfo([VarStruct(u'Translation', [0, 1200])])
]
)
+68 -134
View File
@@ -202,8 +202,6 @@ class YoutubeDL(object):
logtostderr: Log messages to stderr instead of stdout. logtostderr: Log messages to stderr instead of stdout.
writedescription: Write the video description to a .description file writedescription: Write the video description to a .description file
writeinfojson: Write the video description to a .info.json file writeinfojson: Write the video description to a .info.json file
writecomments: Extract video comments. This will not be written to disk
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
write_all_thumbnails: Write all thumbnail formats to files write_all_thumbnails: Write all thumbnail formats to files
@@ -296,9 +294,6 @@ class YoutubeDL(object):
Progress hooks are guaranteed to be called at least once Progress hooks are guaranteed to be called at least once
(with status "finished") if the download is successful. (with status "finished") if the download is successful.
merge_output_format: Extension to use when merging formats. merge_output_format: Extension to use when merging formats.
final_ext: Expected final extension; used to detect when the file was
already downloaded and converted. "merge_output_format" is
replaced by this extension when given
fixup: Automatically correct known faults of the file. fixup: Automatically correct known faults of the file.
One of: One of:
- "never": do nothing - "never": do nothing
@@ -352,7 +347,7 @@ class YoutubeDL(object):
The following options are used by the post processors: The following options are used by the post processors:
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available, prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
otherwise prefer ffmpeg. (avconv support is deprecated) otherwise prefer ffmpeg.
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
to the binary or its containing directory. to the binary or its containing directory.
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case) postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
@@ -380,7 +375,8 @@ class YoutubeDL(object):
params = None params = None
_ies = [] _ies = []
_pps = {'beforedl': [], 'aftermove': [], 'normal': []} _pps = []
_pps_end = []
__prepare_filename_warned = False __prepare_filename_warned = False
_download_retcode = None _download_retcode = None
_num_downloads = None _num_downloads = None
@@ -394,7 +390,8 @@ class YoutubeDL(object):
params = {} params = {}
self._ies = [] self._ies = []
self._ies_instances = {} self._ies_instances = {}
self._pps = {'beforedl': [], 'aftermove': [], 'normal': []} self._pps = []
self._pps_end = []
self.__prepare_filename_warned = False self.__prepare_filename_warned = False
self._post_hooks = [] self._post_hooks = []
self._progress_hooks = [] self._progress_hooks = []
@@ -441,11 +438,6 @@ class YoutubeDL(object):
if self.params.get('geo_verification_proxy') is None: if self.params.get('geo_verification_proxy') is None:
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy'] self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
if self.params.get('final_ext'):
if self.params.get('merge_output_format'):
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']
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"')
@@ -502,13 +494,11 @@ class YoutubeDL(object):
pp_class = get_postprocessor(pp_def_raw['key']) pp_class = get_postprocessor(pp_def_raw['key'])
pp_def = dict(pp_def_raw) pp_def = dict(pp_def_raw)
del pp_def['key'] del pp_def['key']
if 'when' in pp_def: after_move = pp_def.get('_after_move', False)
when = pp_def['when'] if '_after_move' in pp_def:
del pp_def['when'] del pp_def['_after_move']
else:
when = 'normal'
pp = pp_class(self, **compat_kwargs(pp_def)) pp = pp_class(self, **compat_kwargs(pp_def))
self.add_post_processor(pp, when=when) self.add_post_processor(pp, after_move=after_move)
for ph in self.params.get('post_hooks', []): for ph in self.params.get('post_hooks', []):
self.add_post_hook(ph) self.add_post_hook(ph)
@@ -560,9 +550,12 @@ class YoutubeDL(object):
for ie in gen_extractor_classes(): for ie in gen_extractor_classes():
self.add_info_extractor(ie) self.add_info_extractor(ie)
def add_post_processor(self, pp, when='normal'): def add_post_processor(self, pp, after_move=False):
"""Add a PostProcessor object to the end of the chain.""" """Add a PostProcessor object to the end of the chain."""
self._pps[when].append(pp) if after_move:
self._pps_end.append(pp)
else:
self._pps.append(pp)
pp.set_downloader(self) pp.set_downloader(self)
def add_post_hook(self, ph): def add_post_hook(self, ph):
@@ -940,7 +933,9 @@ class YoutubeDL(object):
self.to_screen("[%s] %s: has already been recorded in archive" % ( self.to_screen("[%s] %s: has already been recorded in archive" % (
ie_key, temp_id)) ie_key, temp_id))
break break
return self.__extract_info(url, ie, download, extra_info, process, info_dict) return self.__extract_info(url, ie, download, extra_info, process, info_dict)
else: else:
self.report_error('no suitable InfoExtractor for URL %s' % url) self.report_error('no suitable InfoExtractor for URL %s' % url)
@@ -1108,54 +1103,6 @@ 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):
return make_dir(path, self.report_error)
if self.params.get('writeinfojson', False):
infofn = replace_extension(
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):
descfn = replace_extension(
self.prepare_filepath(self.prepare_filename(ie_copy), 'description'),
'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
playlist_results = [] playlist_results = []
@@ -2001,8 +1948,6 @@ class YoutubeDL(object):
self._num_downloads += 1 self._num_downloads += 1
info_dict = self.pre_process(info_dict)
filename = self.prepare_filename(info_dict, warn=True) filename = self.prepare_filename(info_dict, warn=True)
info_dict['_filename'] = full_filename = self.prepare_filepath(filename) info_dict['_filename'] = full_filename = self.prepare_filepath(filename)
temp_filename = self.prepare_filepath(filename, 'temp') temp_filename = self.prepare_filepath(filename, 'temp')
@@ -2153,15 +2098,14 @@ class YoutubeDL(object):
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)):
self.to_screen('[info] Video metadata is already present') self.to_screen('[info] Video description metadata is already present')
else: else:
self.to_screen('[info] Writing video metadata as JSON to: ' + infofn) self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
try: try:
write_json_file(self.filter_requested_info(info_dict), infofn) write_json_file(self.filter_requested_info(info_dict), infofn)
except (OSError, IOError): except (OSError, IOError):
self.report_error('Cannot write video metadata to JSON file ' + infofn) self.report_error('Cannot write metadata to JSON file ' + infofn)
return return
info_dict['__infojson_filepath'] = infofn
thumbdir = os.path.dirname(self.prepare_filepath(filename, 'thumbnail')) thumbdir = os.path.dirname(self.prepare_filepath(filename, 'thumbnail'))
for thumbfn in self._write_thumbnails(info_dict, temp_filename): for thumbfn in self._write_thumbnails(info_dict, temp_filename):
@@ -2221,27 +2165,22 @@ class YoutubeDL(object):
if not self.params.get('skip_download', False): if not self.params.get('skip_download', False):
try: try:
def existing_file(*filepaths): def existing_file(filename, temp_filename):
ext = info_dict.get('ext') file_exists = os.path.exists(encodeFilename(filename))
final_ext = self.params.get('final_ext', ext) tempfile_exists = (
existing_files = [] False if temp_filename == filename
for file in orderedSet(filepaths): else os.path.exists(encodeFilename(temp_filename)))
if final_ext != ext: if not self.params.get('overwrites', False) and (file_exists or tempfile_exists):
converted = replace_extension(file, final_ext, ext) existing_filename = temp_filename if tempfile_exists else filename
if os.path.exists(encodeFilename(converted)): self.to_screen('[download] %s has already been downloaded and merged' % existing_filename)
existing_files.append(converted) return existing_filename
if os.path.exists(encodeFilename(file)): if tempfile_exists:
existing_files.append(file) self.report_file_delete(temp_filename)
os.remove(encodeFilename(temp_filename))
if not existing_files or self.params.get('overwrites', False): if file_exists:
for file in orderedSet(existing_files): self.report_file_delete(filename)
self.report_file_delete(file) os.remove(encodeFilename(filename))
os.remove(encodeFilename(file)) return None
return None
self.report_file_already_downloaded(existing_files[0])
info_dict['ext'] = os.path.splitext(existing_files[0])[1][1:]
return existing_files[0]
success = True success = True
if info_dict.get('requested_formats') is not None: if info_dict.get('requested_formats') is not None:
@@ -2250,7 +2189,7 @@ class YoutubeDL(object):
if not merger.available: if not merger.available:
postprocessors = [] postprocessors = []
self.report_warning('You have requested multiple ' self.report_warning('You have requested multiple '
'formats but ffmpeg is not installed.' 'formats but ffmpeg or avconv are not installed.'
' The formats won\'t be merged.') ' The formats won\'t be merged.')
else: else:
postprocessors = [merger] postprocessors = [merger]
@@ -2334,7 +2273,7 @@ class YoutubeDL(object):
if fixup_policy is None: if fixup_policy is None:
fixup_policy = 'detect_or_warn' fixup_policy = 'detect_or_warn'
INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg to fix this automatically.' INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg or avconv to fix this automatically.'
stretched_ratio = info_dict.get('stretched_ratio') stretched_ratio = info_dict.get('stretched_ratio')
if stretched_ratio is not None and stretched_ratio != 1: if stretched_ratio is not None and stretched_ratio != 1:
@@ -2353,8 +2292,7 @@ class YoutubeDL(object):
assert fixup_policy in ('ignore', 'never') assert fixup_policy in ('ignore', 'never')
if (info_dict.get('requested_formats') is None if (info_dict.get('requested_formats') is None
and info_dict.get('container') == 'm4a_dash' and info_dict.get('container') == 'm4a_dash'):
and info_dict.get('ext') == 'm4a'):
if fixup_policy == 'warn': if fixup_policy == 'warn':
self.report_warning( self.report_warning(
'%s: writing DASH m4a. ' '%s: writing DASH m4a. '
@@ -2462,45 +2400,41 @@ class YoutubeDL(object):
(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 not in ['requested_formats', 'requested_subtitles'])
def run_pp(self, pp, infodict, files_to_move={}):
files_to_delete = []
try:
files_to_delete, infodict = pp.run(infodict)
except PostProcessingError as e:
self.report_error(e.msg)
if not files_to_delete:
return files_to_move, infodict
if self.params.get('keepvideo', False):
for f in files_to_delete:
files_to_move.setdefault(f, '')
else:
for old_filename in set(files_to_delete):
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
try:
os.remove(encodeFilename(old_filename))
except (IOError, OSError):
self.report_warning('Unable to remove downloaded original file')
if old_filename in files_to_move:
del files_to_move[old_filename]
return files_to_move, infodict
def pre_process(self, ie_info):
info = dict(ie_info)
for pp in self._pps['beforedl']:
info = self.run_pp(pp, info)[1]
return info
def post_process(self, filename, ie_info, files_to_move={}): def post_process(self, filename, ie_info, files_to_move={}):
"""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
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']: def run_pp(pp):
files_to_move, info = self.run_pp(pp, info, files_to_move) files_to_delete = []
info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info, files_to_move)[1] infodict = info
for pp in self._pps['aftermove']: try:
files_to_move, info = self.run_pp(pp, info, {}) files_to_delete, infodict = pp.run(infodict)
except PostProcessingError as e:
self.report_error(e.msg)
if not files_to_delete:
return infodict
if self.params.get('keepvideo', False):
for f in files_to_delete:
files_to_move.setdefault(f, '')
else:
for old_filename in set(files_to_delete):
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
try:
os.remove(encodeFilename(old_filename))
except (IOError, OSError):
self.report_warning('Unable to remove downloaded original file')
if old_filename in files_to_move:
del files_to_move[old_filename]
return infodict
for pp in ie_info.get('__postprocessors', []) + self._pps:
info = run_pp(pp)
info = run_pp(MoveFilesAfterDownloadPP(self, files_to_move))
files_to_move = {}
for pp in self._pps_end:
info = run_pp(pp)
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')
+10 -28
View File
@@ -15,7 +15,6 @@ import sys
from .options import ( from .options import (
parseOpts, parseOpts,
_remux_formats,
) )
from .compat import ( from .compat import (
compat_getpass, compat_getpass,
@@ -46,7 +45,6 @@ from .downloader import (
from .extractor import gen_extractors, list_extractors from .extractor import gen_extractors, list_extractors
from .extractor.common import InfoExtractor from .extractor.common import InfoExtractor
from .extractor.adobepass import MSO_INFO from .extractor.adobepass import MSO_INFO
from .postprocessor.metadatafromfield import MetadataFromFieldPP
from .YoutubeDL import YoutubeDL from .YoutubeDL import YoutubeDL
@@ -210,15 +208,12 @@ def _real_main(argv=None):
opts.audioquality = opts.audioquality.strip('k').strip('K') opts.audioquality = opts.audioquality.strip('k').strip('K')
if not opts.audioquality.isdigit(): if not opts.audioquality.isdigit():
parser.error('invalid audio quality specified') parser.error('invalid audio quality specified')
if opts.recodevideo is not None:
if opts.recodevideo not in _remux_formats:
parser.error('invalid video recode format specified')
if opts.remuxvideo and opts.recodevideo:
opts.remuxvideo = None
write_string('WARNING: --remux-video is ignored since --recode-video was given\n', out=sys.stderr)
if opts.remuxvideo is not None: if opts.remuxvideo is not None:
if opts.remuxvideo not in _remux_formats: if opts.remuxvideo not in ['mp4', 'mkv']:
parser.error('invalid video remux format specified') parser.error('invalid video container format specified')
if opts.recodevideo is not None:
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv', 'avi']:
parser.error('invalid video recode format specified')
if opts.convertsubtitles is not None: if opts.convertsubtitles is not None:
if opts.convertsubtitles not in ['srt', 'vtt', 'ass', 'lrc']: if opts.convertsubtitles not in ['srt', 'vtt', 'ass', 'lrc']:
parser.error('invalid subtitle format specified') parser.error('invalid subtitle format specified')
@@ -254,25 +249,16 @@ def _real_main(argv=None):
if re.match(InfoExtractor.FormatSort.regex, f) is None: if re.match(InfoExtractor.FormatSort.regex, f) is None:
parser.error('invalid format sort string "%s" specified' % f) parser.error('invalid format sort string "%s" specified' % f)
if opts.metafromfield is None:
opts.metafromfield = []
if opts.metafromtitle is not None:
opts.metafromfield.append('title:%s' % opts.metafromtitle)
for f in opts.metafromfield:
if re.match(MetadataFromFieldPP.regex, f) is None:
parser.error('invalid format string "%s" specified for --parse-metadata' % f)
any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json
any_printing = opts.print_json any_printing = opts.print_json
download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive
# PostProcessors # PostProcessors
postprocessors = [] postprocessors = []
if opts.metafromfield: if opts.metafromtitle:
postprocessors.append({ postprocessors.append({
'key': 'MetadataFromField', 'key': 'MetadataFromTitle',
'formats': opts.metafromfield, 'titleformat': opts.metafromtitle
'when': 'beforedl'
}) })
if opts.extractaudio: if opts.extractaudio:
postprocessors.append({ postprocessors.append({
@@ -338,7 +324,7 @@ def _real_main(argv=None):
postprocessors.append({ postprocessors.append({
'key': 'ExecAfterDownload', 'key': 'ExecAfterDownload',
'exec_cmd': opts.exec_cmd, 'exec_cmd': opts.exec_cmd,
'when': 'aftermove' '_after_move': True
}) })
_args_compat_warning = 'WARNING: %s given without specifying name. The arguments will be given to all %s\n' _args_compat_warning = 'WARNING: %s given without specifying name. The arguments will be given to all %s\n'
@@ -350,8 +336,6 @@ def _real_main(argv=None):
opts.postprocessor_args.setdefault('sponskrub', []) opts.postprocessor_args.setdefault('sponskrub', [])
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat'] opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
audio_ext = opts.audioformat if (opts.extractaudio and opts.audioformat != 'best') else None
match_filter = ( match_filter = (
None if opts.match_filter is None None if opts.match_filter is None
else match_filter_func(opts.match_filter)) else match_filter_func(opts.match_filter))
@@ -419,8 +403,7 @@ def _real_main(argv=None):
'updatetime': opts.updatetime, 'updatetime': opts.updatetime,
'writedescription': opts.writedescription, 'writedescription': opts.writedescription,
'writeannotations': opts.writeannotations, 'writeannotations': opts.writeannotations,
'writeinfojson': opts.writeinfojson or opts.getcomments, 'writeinfojson': opts.writeinfojson,
'getcomments': opts.getcomments,
'writethumbnail': opts.writethumbnail, 'writethumbnail': opts.writethumbnail,
'write_all_thumbnails': opts.write_all_thumbnails, 'write_all_thumbnails': opts.write_all_thumbnails,
'writelink': opts.writelink, 'writelink': opts.writelink,
@@ -471,7 +454,6 @@ def _real_main(argv=None):
'extract_flat': opts.extract_flat, 'extract_flat': opts.extract_flat,
'mark_watched': opts.mark_watched, 'mark_watched': opts.mark_watched,
'merge_output_format': opts.merge_output_format, 'merge_output_format': opts.merge_output_format,
'final_ext': opts.recodevideo or opts.remuxvideo or audio_ext,
'postprocessors': postprocessors, 'postprocessors': postprocessors,
'fixup': opts.fixup, 'fixup': opts.fixup,
'source_address': opts.source_address, 'source_address': opts.source_address,
+1 -1
View File
@@ -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 or avconv could not be found. Please install one.')
return False return False
ffpp.check_version() ffpp.check_version()
+1 -2
View File
@@ -7,10 +7,9 @@ try:
from .lazy_extractors import _ALL_CLASSES from .lazy_extractors import _ALL_CLASSES
_LAZY_LOADER = True _LAZY_LOADER = True
_PLUGIN_CLASSES = [] _PLUGIN_CLASSES = []
except ImportError: except ImportError:
_LAZY_LOADER = False _LAZY_LOADER = False
if not _LAZY_LOADER:
from .extractors import * from .extractors import *
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals()) _PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
+3 -20
View File
@@ -21,16 +21,6 @@ from ..utils import (
unsmuggle_url, unsmuggle_url,
) )
# This import causes a ModuleNotFoundError on some systems for unknown reason.
# See issues:
# https://github.com/pukkandan/yt-dlp/issues/35
# https://github.com/ytdl-org/youtube-dl/issues/27449
# https://github.com/animelover1984/youtube-dl/issues/17
try:
from .anvato_token_generator import NFLTokenGenerator
except ImportError:
NFLTokenGenerator = None
def md5_text(s): def md5_text(s):
if not isinstance(s, compat_str): if not isinstance(s, compat_str):
@@ -213,10 +203,6 @@ class AnvatoIE(InfoExtractor):
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582' 'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582'
} }
_TOKEN_GENERATORS = {
'GXvEgwyJeWem8KCYXfeoHWknwP48Mboj': NFLTokenGenerator,
}
_API_KEY = '3hwbSuqqT690uxjNYBktSQpa5ZrpYYR0Iofx7NcJHyA' _API_KEY = '3hwbSuqqT690uxjNYBktSQpa5ZrpYYR0Iofx7NcJHyA'
_ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1' _ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1'
@@ -276,12 +262,9 @@ class AnvatoIE(InfoExtractor):
'anvrid': anvrid, 'anvrid': anvrid,
'anvts': server_time, 'anvts': server_time,
} }
if self._TOKEN_GENERATORS.get(access_key) is not None: api['anvstk'] = md5_text('%s|%s|%d|%s' % (
api['anvstk2'] = self._TOKEN_GENERATORS[access_key].generate(self, access_key, video_id) access_key, anvrid, server_time,
else: self._ANVACK_TABLE.get(access_key, self._API_KEY)))
api['anvstk'] = md5_text('%s|%s|%d|%s' % (
access_key, anvrid, server_time,
self._ANVACK_TABLE.get(access_key, self._API_KEY)))
return self._download_json( return self._download_json(
video_data_url, video_id, transform_source=strip_jsonp, video_data_url, video_id, transform_source=strip_jsonp,
-224
View File
@@ -1,224 +0,0 @@
# coding: utf-8
from __future__ import unicode_literals
import random
import re
from .common import InfoExtractor
from ..utils import ExtractorError, try_get, compat_str, str_or_none
from ..compat import compat_urllib_parse_unquote
class AudiusBaseIE(InfoExtractor):
_API_BASE = None
_API_V = '/v1'
def _get_response_data(self, response):
if isinstance(response, dict):
response_data = response.get('data')
if response_data is not None:
return response_data
if len(response) == 1 and 'message' in response:
raise ExtractorError('API error: %s' % response['message'],
expected=True)
raise ExtractorError('Unexpected API response')
def _select_api_base(self):
"""Selecting one of the currently available API hosts"""
response = super(AudiusBaseIE, self)._download_json(
'https://api.audius.co/', None,
note='Requesting available API hosts',
errnote='Unable to request available API hosts')
hosts = self._get_response_data(response)
if isinstance(hosts, list):
self._API_BASE = random.choice(hosts)
return
raise ExtractorError('Unable to get available API hosts')
@staticmethod
def _prepare_url(url, title):
"""
Audius removes forward slashes from the uri, but leaves backslashes.
The problem is that the current version of Chrome replaces backslashes
in the address bar with a forward slashes, so if you copy the link from
there and paste it into youtube-dl, you won't be able to download
anything from this link, since the Audius API won't be able to resolve
this url
"""
url = compat_urllib_parse_unquote(url)
title = compat_urllib_parse_unquote(title)
if '/' in title or '%2F' in title:
fixed_title = title.replace('/', '%5C').replace('%2F', '%5C')
return url.replace(title, fixed_title)
return url
def _api_request(self, path, item_id=None, note='Downloading JSON metadata',
errnote='Unable to download JSON metadata',
expected_status=None):
if self._API_BASE is None:
self._select_api_base()
try:
response = super(AudiusBaseIE, self)._download_json(
'%s%s%s' % (self._API_BASE, self._API_V, path), item_id, note=note,
errnote=errnote, expected_status=expected_status)
except ExtractorError as exc:
# some of Audius API hosts may not work as expected and return HTML
if 'Failed to parse JSON' in compat_str(exc):
raise ExtractorError('An error occurred while receiving data. Try again',
expected=True)
raise exc
return self._get_response_data(response)
def _resolve_url(self, url, item_id):
return self._api_request('/resolve?url=%s' % url, item_id,
expected_status=404)
class AudiusIE(AudiusBaseIE):
_VALID_URL = r'''(?x)https?://(?:www\.)?(?:audius\.co/(?P<uploader>[\w\d-]+)(?!/album|/playlist)/(?P<title>\S+))'''
_TESTS = [
{
# URL from Chrome address bar which replace backslash to forward slash
'url': 'https://audius.co/test_acc/t%D0%B5%D0%B5%D0%B5est-1.%5E_%7B%7D/%22%3C%3E.%E2%84%96~%60-198631',
'md5': '92c35d3e754d5a0f17eef396b0d33582',
'info_dict': {
'id': 'xd8gY',
'title': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
'ext': 'mp3',
'description': 'Description',
'duration': 30,
'track': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
'artist': 'test',
'genre': 'Electronic',
'thumbnail': r're:https?://.*\.jpg',
'view_count': int,
'like_count': int,
'repost_count': int,
}
},
{
# Regular track
'url': 'https://audius.co/voltra/radar-103692',
'md5': '491898a0a8de39f20c5d6a8a80ab5132',
'info_dict': {
'id': 'KKdy2',
'title': 'RADAR',
'ext': 'mp3',
'duration': 318,
'track': 'RADAR',
'artist': 'voltra',
'genre': 'Trance',
'thumbnail': r're:https?://.*\.jpg',
'view_count': int,
'like_count': int,
'repost_count': int,
}
},
]
_ARTWORK_MAP = {
"150x150": 150,
"480x480": 480,
"1000x1000": 1000
}
def _real_extract(self, url):
mobj = re.match(self._VALID_URL, url)
uploader, title, track_id = mobj.groups()
if track_id is None:
url = self._prepare_url(url, title)
track_data = self._resolve_url(url, title)
else: # API link
track_data = self._api_request('/tracks/%s' % track_id, track_id)
if not isinstance(track_data, dict):
raise ExtractorError('Unexpected API response')
track_id = track_data.get('id')
if track_id is None:
raise ExtractorError('Unable to get ID of the track')
artworks_data = track_data.get('artwork')
thumbnails = []
if isinstance(artworks_data, dict):
for quality_key, thumbnail_url in artworks_data.items():
thumbnail = {
"url": thumbnail_url
}
quality_code = self._ARTWORK_MAP.get(quality_key)
if quality_code is not None:
thumbnail['preference'] = quality_code
thumbnails.append(thumbnail)
return {
'id': track_id,
'title': track_data.get('title', title),
'url': '%s/v1/tracks/%s/stream' % (self._API_BASE, track_id),
'ext': 'mp3',
'description': track_data.get('description'),
'duration': track_data.get('duration'),
'track': track_data.get('title'),
'artist': try_get(track_data, lambda x: x['user']['name'], compat_str),
'genre': track_data.get('genre'),
'thumbnails': thumbnails,
'view_count': track_data.get('play_count'),
'like_count': track_data.get('favorite_count'),
'repost_count': track_data.get('repost_count'),
}
class AudiusPlaylistIE(AudiusBaseIE):
_VALID_URL = r'https?://(?:www\.)?audius\.co/(?P<uploader>[\w\d-]+)/(?:album|playlist)/(?P<title>\S+)'
IE_NAME = 'audius:playlist'
_TEST = {
'url': 'https://audius.co/test_acc/playlist/test-playlist-22910',
'info_dict': {
'id': 'DNvjN',
'title': 'test playlist',
'description': 'Test description\n\nlol',
},
'playlist_count': 175,
}
def _build_playlist(self, tracks):
entries = []
for track in tracks:
if not isinstance(track, dict):
raise ExtractorError('Unexpected API response')
track_id = str_or_none(track.get('id'))
if not track_id:
raise ExtractorError('Unable to get track ID from playlist')
entries.append(self.url_result(
'%s%s/tracks/%s' % (self._API_BASE, self._API_V, track_id),
ie=AudiusIE.ie_key(), video_id=track_id))
return entries
def _real_extract(self, url):
self._select_api_base()
mobj = re.match(self._VALID_URL, url)
uploader, title = mobj.groups()
url = self._prepare_url(url, title)
playlist_response = self._resolve_url(url, title)
if not isinstance(playlist_response, list) or len(playlist_response) != 1:
raise ExtractorError('Unexpected API response')
playlist_data = playlist_response[0]
if not isinstance(playlist_data, dict):
raise ExtractorError('Unexpected API response')
playlist_id = playlist_data.get('id')
if playlist_id is None:
raise ExtractorError('Unable to get playlist ID')
playlist_tracks = self._api_request(
'/playlists/%s/tracks' % playlist_id,
title, note='Downloading playlist tracks metadata',
errnote='Unable to download playlist tracks metadata')
if not isinstance(playlist_tracks, list):
raise ExtractorError('Unexpected API response')
entries = self._build_playlist(playlist_tracks)
return self.playlist_result(entries, playlist_id,
playlist_data.get('playlist_name', title),
playlist_data.get('description'))
+10 -199
View File
@@ -2,10 +2,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib import hashlib
import json
import re import re
from .common import InfoExtractor, SearchInfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_parse_qs, compat_parse_qs,
compat_urlparse, compat_urlparse,
@@ -33,14 +32,13 @@ class BiliBiliIE(InfoExtractor):
(?: (?:
video/[aA][vV]| video/[aA][vV]|
anime/(?P<anime_id>\d+)/play\# anime/(?P<anime_id>\d+)/play\#
)(?P<id>\d+)| )(?P<id_bv>\d+)|
video/[bB][vV](?P<id_bv>[^/?#&]+) video/[bB][vV](?P<id>[^/?#&]+)
) )
(?:/?\?p=(?P<page>\d+))?
''' '''
_TESTS = [{ _TESTS = [{
'url': 'http://www.bilibili.com/video/av1074402/', 'url': 'http://www.bilibili.tv/video/av1074402/',
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788', 'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
'info_dict': { 'info_dict': {
'id': '1074402', 'id': '1074402',
@@ -58,10 +56,6 @@ class BiliBiliIE(InfoExtractor):
# Tested in BiliBiliBangumiIE # Tested in BiliBiliBangumiIE
'url': 'http://bangumi.bilibili.com/anime/1869/play#40062', 'url': 'http://bangumi.bilibili.com/anime/1869/play#40062',
'only_matching': True, 'only_matching': True,
}, {
# bilibili.tv
'url': 'http://www.bilibili.tv/video/av1074402/',
'only_matching': True,
}, { }, {
'url': 'http://bangumi.bilibili.com/anime/5802/play#100643', 'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
'md5': '3f721ad1e75030cc06faf73587cfec57', 'md5': '3f721ad1e75030cc06faf73587cfec57',
@@ -130,20 +124,12 @@ class BiliBiliIE(InfoExtractor):
url, smuggled_data = unsmuggle_url(url, {}) url, smuggled_data = unsmuggle_url(url, {})
mobj = re.match(self._VALID_URL, url) mobj = re.match(self._VALID_URL, url)
video_id = mobj.group('id_bv') or mobj.group('id') video_id = mobj.group('id') or mobj.group('id_bv')
av_id, bv_id = self._get_video_id_set(video_id, mobj.group('id_bv') is not None)
video_id = av_id
anime_id = mobj.group('anime_id') anime_id = mobj.group('anime_id')
page_id = mobj.group('page')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
if 'anime/' not in url: if 'anime/' not in url:
cid = self._search_regex( cid = self._search_regex(
r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + str(page_id), webpage, 'cid',
default=None
) or self._search_regex(
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid', r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
default=None default=None
) or compat_parse_qs(self._search_regex( ) or compat_parse_qs(self._search_regex(
@@ -221,9 +207,9 @@ class BiliBiliIE(InfoExtractor):
break break
title = self._html_search_regex( title = self._html_search_regex(
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1', ('<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title', '(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
group='title') + ('_p' + str(page_id) if page_id is not None else '') group='title')
description = self._html_search_meta('description', webpage) description = self._html_search_meta('description', webpage)
timestamp = unified_timestamp(self._html_search_regex( timestamp = unified_timestamp(self._html_search_regex(
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time', r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
@@ -233,8 +219,7 @@ class BiliBiliIE(InfoExtractor):
# TODO 'view_count' requires deobfuscating Javascript # TODO 'view_count' requires deobfuscating Javascript
info = { info = {
'id': str(video_id) if page_id is None else '%s_p%s' % (video_id, page_id), 'id': video_id,
'cid': cid,
'title': title, 'title': title,
'description': description, 'description': description,
'timestamp': timestamp, 'timestamp': timestamp,
@@ -250,134 +235,27 @@ class BiliBiliIE(InfoExtractor):
'uploader': uploader_mobj.group('name'), 'uploader': uploader_mobj.group('name'),
'uploader_id': uploader_mobj.group('id'), 'uploader_id': uploader_mobj.group('id'),
}) })
if not info.get('uploader'): if not info.get('uploader'):
info['uploader'] = self._html_search_meta( info['uploader'] = self._html_search_meta(
'author', webpage, 'uploader', default=None) 'author', webpage, 'uploader', default=None)
comments = None
if self._downloader.params.get('getcomments', False):
comments = self._get_all_comment_pages(video_id)
raw_danmaku = self._get_raw_danmaku(video_id, cid)
raw_tags = self._get_tags(video_id)
tags = list(map(lambda x: x['tag_name'], raw_tags))
top_level_info = {
'raw_danmaku': raw_danmaku,
'comments': comments,
'comment_count': len(comments) if comments is not None else None,
'tags': tags,
'raw_tags': raw_tags,
}
'''
# Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3
# See https://github.com/animelover1984/youtube-dl
danmaku = NiconicoIE.CreateDanmaku(raw_danmaku, commentType='Bilibili', x=1024, y=576)
entries[0]['subtitles'] = {
'danmaku': [{
'ext': 'ass',
'data': danmaku
}]
}
'''
for entry in entries: for entry in entries:
entry.update(info) entry.update(info)
if len(entries) == 1: if len(entries) == 1:
entries[0].update(top_level_info)
return entries[0] return entries[0]
else: else:
for idx, entry in enumerate(entries): for idx, entry in enumerate(entries):
entry['id'] = '%s_part%d' % (video_id, (idx + 1)) entry['id'] = '%s_part%d' % (video_id, (idx + 1))
global_info = { return {
'_type': 'multi_video', '_type': 'multi_video',
'id': video_id, 'id': video_id,
'bv_id': bv_id,
'title': title, 'title': title,
'description': description, 'description': description,
'entries': entries, 'entries': entries,
} }
global_info.update(info)
global_info.update(top_level_info)
return global_info
def _get_video_id_set(self, id, is_bv):
query = {'bvid': id} if is_bv else {'aid': id}
response = self._download_json(
"http://api.bilibili.cn/x/web-interface/view",
id, query=query,
note='Grabbing original ID via API')
if response['code'] == -400:
raise ExtractorError('Video ID does not exist', expected=True, video_id=id)
elif response['code'] != 0:
raise ExtractorError('Unknown error occurred during API check (code %s)' % response['code'], expected=True, video_id=id)
return (response['data']['aid'], response['data']['bvid'])
# recursive solution to getting every page of comments for the video
# we can stop when we reach a page without any comments
def _get_all_comment_pages(self, video_id, commentPageNumber=0):
comment_url = "https://api.bilibili.com/x/v2/reply?jsonp=jsonp&pn=%s&type=1&oid=%s&sort=2&_=1567227301685" % (commentPageNumber, video_id)
json_str = self._download_webpage(
comment_url, video_id,
note='Extracting comments from page %s' % (commentPageNumber))
replies = json.loads(json_str)['data']['replies']
if replies is None:
return []
return self._get_all_children(replies) + self._get_all_comment_pages(video_id, commentPageNumber + 1)
# extracts all comments in the tree
def _get_all_children(self, replies):
if replies is None:
return []
ret = []
for reply in replies:
author = reply['member']['uname']
author_id = reply['member']['mid']
id = reply['rpid']
text = reply['content']['message']
timestamp = reply['ctime']
parent = reply['parent'] if reply['parent'] != 0 else 'root'
comment = {
"author": author,
"author_id": author_id,
"id": id,
"text": text,
"timestamp": timestamp,
"parent": parent,
}
ret.append(comment)
# from the JSON, the comment structure seems arbitrarily deep, but I could be wrong.
# Regardless, this should work.
ret += self._get_all_children(reply['replies'])
return ret
def _get_raw_danmaku(self, video_id, cid):
# This will be useful if I decide to scrape all pages instead of doing them individually
# cid_url = "https://www.bilibili.com/widget/getPageList?aid=%s" % (video_id)
# cid_str = self._download_webpage(cid_url, video_id, note=False)
# cid = json.loads(cid_str)[0]['cid']
danmaku_url = "https://comment.bilibili.com/%s.xml" % (cid)
danmaku = self._download_webpage(danmaku_url, video_id, note='Downloading danmaku comments')
return danmaku
def _get_tags(self, video_id):
tags_url = "https://api.bilibili.com/x/tag/archive/tags?aid=%s" % (video_id)
tags_json = self._download_json(tags_url, video_id, note='Downloading tags')
return tags_json['data']
class BiliBiliBangumiIE(InfoExtractor): class BiliBiliBangumiIE(InfoExtractor):
_VALID_URL = r'https?://bangumi\.bilibili\.com/anime/(?P<id>\d+)' _VALID_URL = r'https?://bangumi\.bilibili\.com/anime/(?P<id>\d+)'
@@ -446,73 +324,6 @@ class BiliBiliBangumiIE(InfoExtractor):
season_info.get('bangumi_title'), season_info.get('evaluate')) season_info.get('bangumi_title'), season_info.get('evaluate'))
class BilibiliChannelIE(InfoExtractor):
_VALID_URL = r'https?://space.bilibili\.com/(?P<id>\d+)'
# May need to add support for pagination? Need to find a user with many video uploads to test
_API_URL = "https://api.bilibili.com/x/space/arc/search?mid=%s&pn=1&ps=25&jsonp=jsonp"
_TEST = {} # TODO: Add tests
def _real_extract(self, url):
list_id = self._match_id(url)
json_str = self._download_webpage(self._API_URL % list_id, "None")
json_parsed = json.loads(json_str)
entries = [{
'_type': 'url',
'ie_key': BiliBiliIE.ie_key(),
'url': ('https://www.bilibili.com/video/%s' %
entry['bvid']),
'id': entry['bvid'],
} for entry in json_parsed['data']['list']['vlist']]
return {
'_type': 'playlist',
'id': list_id,
'entries': entries
}
class BiliBiliSearchIE(SearchInfoExtractor):
IE_DESC = 'Bilibili video search, "bilisearch" keyword'
_MAX_RESULTS = 100000
_SEARCH_KEY = 'bilisearch'
MAX_NUMBER_OF_RESULTS = 1000
def _get_n_results(self, query, n):
"""Get a specified number of results for a query"""
entries = []
pageNumber = 0
while True:
pageNumber += 1
# FIXME
api_url = "https://api.bilibili.com/x/web-interface/search/type?context=&page=%s&order=pubdate&keyword=%s&duration=0&tids_2=&__refresh__=true&search_type=video&tids=0&highlight=1" % (pageNumber, query)
json_str = self._download_webpage(
api_url, "None", query={"Search_key": query},
note='Extracting results from page %s' % pageNumber)
data = json.loads(json_str)['data']
# FIXME: this is hideous
if "result" not in data:
return {
'_type': 'playlist',
'id': query,
'entries': entries[:n]
}
videos = data['result']
for video in videos:
e = self.url_result(video['arcurl'], 'BiliBili', str(video['aid']))
entries.append(e)
if(len(entries) >= n or len(videos) >= BiliBiliSearchIE.MAX_NUMBER_OF_RESULTS):
return {
'_type': 'playlist',
'id': query,
'entries': entries[:n]
}
class BilibiliAudioBaseIE(InfoExtractor): class BilibiliAudioBaseIE(InfoExtractor):
def _call_api(self, path, sid, query=None): def _call_api(self, path, sid, query=None):
if not query: if not query:
+6 -6
View File
@@ -1375,7 +1375,7 @@ class InfoExtractor(object):
'order': ['vp9', '(h265|he?vc?)', '(h264|avc)', 'vp8', '(mp4v|h263)', 'theora', '', None, 'none']}, 'order': ['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,
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', 'm3u8', '.*dash', '', 'mms|rtsp', 'none', 'f4']}, 'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', 'm3u8', '.*dash', '', 'mms|rtsp', 'none', 'f4']},
'vext': {'type': 'ordered', 'field': 'video_ext', 'vext': {'type': 'ordered', 'field': 'video_ext',
'order': ('mp4', 'webm', 'flv', '', 'none'), 'order': ('mp4', 'webm', 'flv', '', 'none'),
@@ -1384,14 +1384,14 @@ class InfoExtractor(object):
'order': ('m4a', 'aac', 'mp3', 'ogg', 'opus', 'webm', '', 'none'), 'order': ('m4a', 'aac', 'mp3', 'ogg', 'opus', 'webm', '', 'none'),
'order_free': ('opus', 'ogg', 'webm', 'm4a', 'mp3', 'aac', '', 'none')}, 'order_free': ('opus', 'ogg', 'webm', 'm4a', 'mp3', 'aac', '', 'none')},
'hidden': {'visible': False, 'forced': True, 'type': 'extractor', 'max': -1000}, 'hidden': {'visible': False, 'forced': True, 'type': 'extractor', 'max': -1000},
'ie_pref': {'priority': True, 'type': 'extractor', 'field': 'extractor_preference'}, 'ie_pref': {'priority': True, 'type': 'extractor'},
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
'lang': {'priority': True, 'convert': 'ignore', 'field': 'language_preference'}, 'lang': {'priority': True, 'convert': 'ignore'},
'quality': {'priority': True, 'convert': 'float_none'}, 'quality': {'priority': True, 'convert': 'float_none'},
'filesize': {'convert': 'bytes'}, 'filesize': {'convert': 'bytes'},
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'}, 'fs_approx': {'convert': 'bytes'},
'id': {'convert': 'string', 'field': 'format_id'}, 'id': {'convert': 'string'},
'height': {'convert': 'float_none'}, 'height': {'convert': 'float_none'},
'width': {'convert': 'float_none'}, 'width': {'convert': 'float_none'},
'fps': {'convert': 'float_none'}, 'fps': {'convert': 'float_none'},
@@ -1399,7 +1399,7 @@ class InfoExtractor(object):
'vbr': {'convert': 'float_none'}, 'vbr': {'convert': 'float_none'},
'abr': {'convert': 'float_none'}, 'abr': {'convert': 'float_none'},
'asr': {'convert': 'float_none'}, 'asr': {'convert': 'float_none'},
'source': {'convert': 'ignore', 'field': 'source_preference'}, 'source': {'convert': 'ignore'},
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')}, 'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True}, 'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
-3
View File
@@ -90,7 +90,6 @@ 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 .awaan import ( from .awaan import (
AWAANIE, AWAANIE,
AWAANVideoIE, AWAANVideoIE,
@@ -123,12 +122,10 @@ from .bigflix import BigflixIE
from .bild import BildIE from .bild import BildIE
from .bilibili import ( from .bilibili import (
BiliBiliIE, BiliBiliIE,
BiliBiliSearchIE,
BiliBiliBangumiIE, BiliBiliBangumiIE,
BilibiliAudioIE, BilibiliAudioIE,
BilibiliAudioAlbumIE, BilibiliAudioAlbumIE,
BiliBiliPlayerIE, BiliBiliPlayerIE,
BilibiliChannelIE,
) )
from .biobiochiletv import BioBioChileTVIE from .biobiochiletv import BioBioChileTVIE
from .bitchute import ( from .bitchute import (
+6 -171
View File
@@ -2424,10 +2424,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
default=None default=None
)) ))
# get xsrf for annotations or comments # annotations
get_annotations = self._downloader.params.get('writeannotations', False) video_annotations = None
get_comments = self._downloader.params.get('getcomments', False) if self._downloader.params.get('writeannotations', False):
if get_annotations or get_comments:
xsrf_token = None xsrf_token = None
ytcfg = self._extract_ytcfg(video_id, video_webpage) ytcfg = self._extract_ytcfg(video_id, video_webpage)
if ytcfg: if ytcfg:
@@ -2436,10 +2435,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
xsrf_token = self._search_regex( xsrf_token = self._search_regex(
r'([\'"])XSRF_TOKEN\1\s*:\s*([\'"])(?P<xsrf_token>(?:(?!\2).)+)\2', r'([\'"])XSRF_TOKEN\1\s*:\s*([\'"])(?P<xsrf_token>(?:(?!\2).)+)\2',
video_webpage, 'xsrf token', group='xsrf_token', fatal=False) video_webpage, 'xsrf token', group='xsrf_token', fatal=False)
# annotations
video_annotations = None
if get_annotations:
invideo_url = try_get( invideo_url = try_get(
player_response, lambda x: x['annotations'][0]['playerAnnotationsUrlsRenderer']['invideoUrl'], compat_str) player_response, lambda x: x['annotations'][0]['playerAnnotationsUrlsRenderer']['invideoUrl'], compat_str)
if xsrf_token and invideo_url: if xsrf_token and invideo_url:
@@ -2459,149 +2454,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
chapters = self._extract_chapters(video_webpage, description_original, video_id, video_duration) chapters = self._extract_chapters(video_webpage, description_original, video_id, video_duration)
# Get comments
# TODO: Refactor and move to seperate function
if get_comments:
expected_video_comment_count = 0
video_comments = []
def find_value(html, key, num_chars=2, separator='"'):
pos_begin = html.find(key) + len(key) + num_chars
pos_end = html.find(separator, pos_begin)
return html[pos_begin: pos_end]
def search_dict(partial, key):
if isinstance(partial, dict):
for k, v in partial.items():
if k == key:
yield v
else:
for o in search_dict(v, key):
yield o
elif isinstance(partial, list):
for i in partial:
for o in search_dict(i, key):
yield o
try:
ncd = next(search_dict(yt_initial_data, 'nextContinuationData'))
continuations = [ncd['continuation']]
# Handle videos where comments have been disabled entirely
except StopIteration:
continuations = []
def get_continuation(continuation, session_token, replies=False):
query = {
'pbj': 1,
'ctoken': continuation,
}
if replies:
query['action_get_comment_replies'] = 1
else:
query['action_get_comments'] = 1
while True:
content, handle = self._download_webpage_handle(
'https://www.youtube.com/comment_service_ajax',
video_id,
note=False,
expected_status=[413],
data=urlencode_postdata({
'session_token': session_token
}),
query=query,
headers={
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
'X-YouTube-Client-Name': '1',
'X-YouTube-Client-Version': '2.20201202.06.01'
}
)
response_code = handle.getcode()
if (response_code == 200):
return self._parse_json(content, video_id)
if (response_code == 413):
return None
raise ExtractorError('Unexpected HTTP error code: %s' % response_code)
first_continuation = True
while continuations:
continuation, itct = continuations.pop()
comment_response = get_continuation(continuation, xsrf_token)
if not comment_response:
continue
if list(search_dict(comment_response, 'externalErrorMessage')):
raise ExtractorError('Error returned from server: ' + next(search_dict(comment_response, 'externalErrorMessage')))
if 'continuationContents' not in comment_response['response']:
# Something is wrong here. Youtube won't accept this continuation token for some reason and responds with a user satisfaction dialog (error?)
continue
# not sure if this actually helps
if 'xsrf_token' in comment_response:
xsrf_token = comment_response['xsrf_token']
item_section = comment_response['response']['continuationContents']['itemSectionContinuation']
if first_continuation:
expected_video_comment_count = int(item_section['header']['commentsHeaderRenderer']['countText']['runs'][0]['text'].replace(' Comments', '').replace('1 Comment', '1').replace(',', ''))
first_continuation = False
if 'contents' not in item_section:
# continuation returned no comments?
# set an empty array as to not break the for loop
item_section['contents'] = []
for meta_comment in item_section['contents']:
comment = meta_comment['commentThreadRenderer']['comment']['commentRenderer']
video_comments.append({
'id': comment['commentId'],
'text': ''.join([c['text'] for c in comment['contentText']['runs']]),
'time_text': ''.join([c['text'] for c in comment['publishedTimeText']['runs']]),
'author': comment.get('authorText', {}).get('simpleText', ''),
'votes': comment.get('voteCount', {}).get('simpleText', '0'),
'author_thumbnail': comment['authorThumbnail']['thumbnails'][-1]['url'],
'parent': 'root'
})
if 'replies' not in meta_comment['commentThreadRenderer']:
continue
reply_continuations = [rcn['nextContinuationData']['continuation'] for rcn in meta_comment['commentThreadRenderer']['replies']['commentRepliesRenderer']['continuations']]
while reply_continuations:
time.sleep(1)
continuation = reply_continuations.pop()
replies_data = get_continuation(continuation, xsrf_token, True)
if not replies_data or 'continuationContents' not in replies_data[1]['response']:
continue
if self._downloader.params.get('verbose', False):
self.to_screen('[debug] Comments downloaded (chain %s) %s of ~%s' % (comment['commentId'], len(video_comments), expected_video_comment_count))
reply_comment_meta = replies_data[1]['response']['continuationContents']['commentRepliesContinuation']
for reply_meta in replies_data[1]['response']['continuationContents']['commentRepliesContinuation']['contents']:
reply_comment = reply_meta['commentRenderer']
video_comments.append({
'id': reply_comment['commentId'],
'text': ''.join([c['text'] for c in reply_comment['contentText']['runs']]),
'time_text': ''.join([c['text'] for c in reply_comment['publishedTimeText']['runs']]),
'author': reply_comment.get('authorText', {}).get('simpleText', ''),
'votes': reply_comment.get('voteCount', {}).get('simpleText', '0'),
'author_thumbnail': reply_comment['authorThumbnail']['thumbnails'][-1]['url'],
'parent': comment['commentId']
})
if 'continuations' not in reply_comment_meta or len(reply_comment_meta['continuations']) == 0:
continue
reply_continuations += [rcn['nextContinuationData']['continuation'] for rcn in reply_comment_meta['continuations']]
self.to_screen('Comments downloaded %s of ~%s' % (len(video_comments), expected_video_comment_count))
if 'continuations' in item_section:
continuations += [ncd['nextContinuationData']['continuation'] for ncd in item_section['continuations']]
time.sleep(1)
self.to_screen('Total comments downloaded %s of ~%s' % (len(video_comments), expected_video_comment_count))
else:
expected_video_comment_count = None
video_comments = None
# Look for the DASH manifest # Look for the DASH manifest
if self._downloader.params.get('youtube_include_dash_manifest', True): if self._downloader.params.get('youtube_include_dash_manifest', True):
dash_mpd_fatal = True dash_mpd_fatal = True
@@ -2720,8 +2572,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'release_year': release_year, 'release_year': release_year,
'subscriber_count': subscriber_count, 'subscriber_count': subscriber_count,
'playable_in_embed': playable_in_embed, 'playable_in_embed': playable_in_embed,
'comments': video_comments,
'comment_count': expected_video_comment_count,
} }
@@ -3426,19 +3276,12 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
title += ' - %s' % tab_title title += ' - %s' % tab_title
description = renderer.get('description') description = renderer.get('description')
playlist_id = renderer.get('externalId') playlist_id = renderer.get('externalId')
# this has thumbnails, but there is currently no thumbnail field for playlists
# sidebar.playlistSidebarRenderer has even more data, but its stucture is more complec
renderer = try_get( renderer = try_get(
data, lambda x: x['microformat']['microformatDataRenderer'], dict) data, lambda x: x['metadata']['playlistMetadataRenderer'], dict)
if not renderer:
renderer = try_get(
data, lambda x: x['metadata']['playlistMetadataRenderer'], dict)
if renderer: if renderer:
title = renderer.get('title') title = renderer.get('title')
description = renderer.get('description') description = None
playlist_id = item_id playlist_id = item_id
if playlist_id is None: if playlist_id is None:
playlist_id = item_id playlist_id = item_id
if title is None: if title is None:
@@ -3528,16 +3371,8 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
webpage = self._download_webpage(url, item_id) webpage = self._download_webpage(url, item_id)
identity_token = self._extract_identity_token(webpage, item_id) identity_token = self._extract_identity_token(webpage, item_id)
data = self._extract_yt_initial_data(item_id, webpage) data = self._extract_yt_initial_data(item_id, webpage)
err_msg = None
for alert_type, alert_message in self._extract_alerts(data): for alert_type, alert_message in self._extract_alerts(data):
if alert_type.lower() == 'error': self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
if err_msg:
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg))
err_msg = alert_message
else:
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
if err_msg:
raise ExtractorError('YouTube said: %s' % err_msg, expected=True)
tabs = try_get( tabs = try_get(
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list) data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
if tabs: if tabs:
+29 -41
View File
@@ -22,9 +22,6 @@ from .utils import (
from .version import __version__ from .version import __version__
_remux_formats = ('mp4', 'mkv', 'flv', 'webm', 'mov', 'avi', 'mp3', 'mka', 'm4a', 'ogg', 'opus')
def _hide_login_info(opts): def _hide_login_info(opts):
PRIVATE_OPTS = set(['-p', '--password', '-u', '--username', '--video-password', '--ap-password', '--ap-username']) PRIVATE_OPTS = set(['-p', '--password', '-u', '--username', '--video-password', '--ap-password', '--ap-username'])
eqre = re.compile('^(?P<key>' + ('|'.join(re.escape(po) for po in PRIVATE_OPTS)) + ')=.+$') eqre = re.compile('^(?P<key>' + ('|'.join(re.escape(po) for po in PRIVATE_OPTS)) + ')=.+$')
@@ -937,10 +934,6 @@ 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(
'--get-comments',
action='store_true', dest='getcomments', default=False,
help='Retrieve video comments to be placed in the .info.json file')
filesystem.add_option( filesystem.add_option(
'--load-info-json', '--load-info', '--load-info-json', '--load-info',
dest='load_info_filename', metavar='FILE', dest='load_info_filename', metavar='FILE',
@@ -1008,28 +1001,24 @@ def parseOpts(overrideArguments=None):
postproc.add_option( postproc.add_option(
'-x', '--extract-audio', '-x', '--extract-audio',
action='store_true', dest='extractaudio', default=False, action='store_true', dest='extractaudio', default=False,
help='Convert video files to audio-only files (requires ffmpeg and ffprobe)') help='Convert video files to audio-only files (requires ffmpeg/avconv and ffprobe/avprobe)')
postproc.add_option( postproc.add_option(
'--audio-format', metavar='FORMAT', dest='audioformat', default='best', '--audio-format', metavar='FORMAT', dest='audioformat', default='best',
help='Specify audio format: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "%default" by default; No effect without -x') help='Specify audio format: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "%default" by default; No effect without -x')
postproc.add_option( postproc.add_option(
'--audio-quality', metavar='QUALITY', '--audio-quality', metavar='QUALITY',
dest='audioquality', default='5', dest='audioquality', default='5',
help='Specify ffmpeg audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default %default)') help='Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default %default)')
postproc.add_option( postproc.add_option(
'--remux-video', '--remux-video',
metavar='FORMAT', dest='remuxvideo', default=None, metavar='FORMAT', dest='remuxvideo', default=None,
help=( help=(
'Remux the video into another container if necessary (currently supported: %s). ' 'Remux the video into another container if necessary (currently supported: mp4|mkv). '
'If target container does not support the video/audio codec, remuxing will fail. ' 'If target container does not support the video/audio codec, remuxing will fail'))
'You can specify multiple rules; eg. "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 '
'and anything else to mkv.' % '|'.join(_remux_formats)))
postproc.add_option( postproc.add_option(
'--recode-video', '--recode-video',
metavar='FORMAT', dest='recodevideo', default=None, metavar='FORMAT', dest='recodevideo', default=None,
help=( help='Re-encode the video into another format if re-encoding is necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)')
'Re-encode the video into another format if re-encoding is necessary. '
'The supported formats are the same as --remux-video'))
postproc.add_option( postproc.add_option(
'--postprocessor-args', '--ppa', '--postprocessor-args', '--ppa',
metavar='NAME:ARGS', dest='postprocessor_args', default={}, type='str', metavar='NAME:ARGS', dest='postprocessor_args', default={}, type='str',
@@ -1041,7 +1030,7 @@ def parseOpts(overrideArguments=None):
'to give the argument to the specified postprocessor/executable. Supported postprocessors are: ' 'to give the argument to the specified postprocessor/executable. Supported postprocessors are: '
'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, ' 'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, '
'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor and EmbedThumbnail. ' 'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor and EmbedThumbnail. '
'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. ' 'The supported executables are: SponSkrub, FFmpeg, FFprobe, avconf, avprobe and AtomicParsley. '
'You can use this option multiple times to give different arguments to different postprocessors. ' 'You can use this option multiple times to give different arguments to different postprocessors. '
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable ' 'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
'only when being used by the specified postprocessor. ' 'only when being used by the specified postprocessor. '
@@ -1089,20 +1078,14 @@ def parseOpts(overrideArguments=None):
postproc.add_option( postproc.add_option(
'--metadata-from-title', '--metadata-from-title',
metavar='FORMAT', dest='metafromtitle', metavar='FORMAT', dest='metafromtitle',
help=optparse.SUPPRESS_HELP)
postproc.add_option(
'--parse-metadata',
metavar='FIELD:FORMAT', dest='metafromfield', action='append',
help=( help=(
'Parse additional metadata like title/artist from other fields. ' 'Parse additional metadata like song title / artist from the video title. '
'Give field name to extract data from, and format of the field seperated by a ":". ' 'The format syntax is the same as --output. Regular expression with '
'Either regular expression with named capture groups or a ' 'named capture groups may also be used. '
'similar syntax to the output template can also be used. ' 'The parsed parameters replace existing values. '
'The parsed parameters replace any existing values and can be use in output template' 'Example: --metadata-from-title "%(artist)s - %(title)s" matches a title like '
'This option can be used multiple times. '
'Example: --parse-metadata "title:%(artist)s - %(title)s" matches a title like '
'"Coldplay - Paradise". ' '"Coldplay - Paradise". '
'Example (regex): --parse-metadata "description:Artist - (?P<artist>.+?)"')) 'Example (regex): --metadata-from-title "(?P<artist>.+?) - (?P<title>.+)"'))
postproc.add_option( postproc.add_option(
'--xattrs', '--xattrs',
action='store_true', dest='xattrs', default=False, action='store_true', dest='xattrs', default=False,
@@ -1117,15 +1100,15 @@ def parseOpts(overrideArguments=None):
postproc.add_option( postproc.add_option(
'--prefer-avconv', '--no-prefer-ffmpeg', '--prefer-avconv', '--no-prefer-ffmpeg',
action='store_false', dest='prefer_ffmpeg', action='store_false', dest='prefer_ffmpeg',
help=optparse.SUPPRESS_HELP) help='Prefer avconv over ffmpeg for running the postprocessors (Alias: --no-prefer-ffmpeg)')
postproc.add_option( postproc.add_option(
'--prefer-ffmpeg', '--no-prefer-avconv', '--prefer-ffmpeg', '--no-prefer-avconv',
action='store_true', dest='prefer_ffmpeg', default=True, action='store_true', dest='prefer_ffmpeg',
help=optparse.SUPPRESS_HELP) help='Prefer ffmpeg over avconv for running the postprocessors (default) (Alias: --no-prefer-avconv)')
postproc.add_option( postproc.add_option(
'--ffmpeg-location', '--avconv-location', metavar='PATH', '--ffmpeg-location', '--avconv-location', metavar='PATH',
dest='ffmpeg_location', dest='ffmpeg_location',
help='Location of the ffmpeg binary; either the path to the binary or its containing directory') help='Location of the ffmpeg/avconv binary; either the path to the binary or its containing directory (Alias: --avconv-location)')
postproc.add_option( postproc.add_option(
'--exec', '--exec',
metavar='CMD', dest='exec_cmd', metavar='CMD', dest='exec_cmd',
@@ -1234,15 +1217,20 @@ def parseOpts(overrideArguments=None):
return return
def read_options(path, user=False): def read_options(path, user=False):
for package in ('yt-dlp', 'youtube-dlc'): func = _readUserConf if user else _readOptions
ext = '' if user else '.conf'
current_path = os.path.join(path, 'yt-dlp%s' % ext)
config = func(current_path, default=None)
if user:
config, current_path = config
if config is None:
current_path = os.path.join(path, 'youtube-dlc%s' % ext)
config = func(current_path, default=None)
if user: if user:
config, current_path = _readUserConf(package, default=None) config, current_path = config
else: if config is None:
current_path = os.path.join(path, '%s.conf' % package) return [], None
config = _readOptions(current_path, default=None) return config, current_path
if config is not None:
return config, current_path
return [], None
configs['portable'], paths['portable'] = read_options(get_executable_path()) configs['portable'], paths['portable'] = read_options(get_executable_path())
if '--ignore-config' in configs['portable']: if '--ignore-config' in configs['portable']:
+1 -3
View File
@@ -16,8 +16,7 @@ from .ffmpeg import (
) )
from .xattrpp import XAttrMetadataPP from .xattrpp import XAttrMetadataPP
from .execafterdownload import ExecAfterDownloadPP from .execafterdownload import ExecAfterDownloadPP
from .metadatafromfield import MetadataFromFieldPP from .metadatafromtitle import MetadataFromTitlePP
from .metadatafromfield import MetadataFromTitlePP
from .movefilesafterdownload import MoveFilesAfterDownloadPP from .movefilesafterdownload import MoveFilesAfterDownloadPP
from .sponskrub import SponSkrubPP from .sponskrub import SponSkrubPP
@@ -40,7 +39,6 @@ __all__ = [
'FFmpegSubtitlesConvertorPP', 'FFmpegSubtitlesConvertorPP',
'FFmpegVideoConvertorPP', 'FFmpegVideoConvertorPP',
'FFmpegVideoRemuxerPP', 'FFmpegVideoRemuxerPP',
'MetadataFromFieldPP',
'MetadataFromTitlePP', 'MetadataFromTitlePP',
'MoveFilesAfterDownloadPP', 'MoveFilesAfterDownloadPP',
'SponSkrubPP', 'SponSkrubPP',
+1 -1
View File
@@ -56,7 +56,7 @@ class PostProcessor(object):
def write_debug(self, text, prefix=True, *args, **kwargs): def write_debug(self, text, prefix=True, *args, **kwargs):
tag = '[debug] ' if prefix else '' tag = '[debug] ' if prefix else ''
if self.get_param('verbose', False) and self._downloader: if self.get_param('verbose', False):
return self._downloader.to_screen('%s%s' % (tag, text), *args, **kwargs) return self._downloader.to_screen('%s%s' % (tag, text), *args, **kwargs)
def get_param(self, name, default=None, *args, **kwargs): def get_param(self, name, default=None, *args, **kwargs):
+31 -87
View File
@@ -4,15 +4,6 @@ from __future__ import unicode_literals
import os import os
import subprocess import subprocess
import struct
import re
import base64
try:
import mutagen
_has_mutagen = True
except ImportError:
_has_mutagen = False
from .ffmpeg import FFmpegPostProcessor from .ffmpeg import FFmpegPostProcessor
@@ -20,12 +11,11 @@ from ..utils import (
check_executable, check_executable,
encodeArgument, encodeArgument,
encodeFilename, encodeFilename,
error_to_compat_str,
PostProcessingError, PostProcessingError,
prepend_extension, prepend_extension,
process_communicate_or_kill,
replace_extension, replace_extension,
shell_quote, shell_quote,
process_communicate_or_kill,
) )
@@ -83,7 +73,6 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
# 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))
thumbnail_filename = thumbnail_jpg_filename thumbnail_filename = thumbnail_jpg_filename
thumbnail_ext = 'jpg'
success = True success = True
if info['ext'] == 'mp3': if info['ext'] == 'mp3':
@@ -94,92 +83,47 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
self.to_screen('Adding thumbnail to "%s"' % filename) self.to_screen('Adding thumbnail to "%s"' % filename)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
elif info['ext'] in ['mkv', 'mka']: elif info['ext'] == 'mkv':
options = ['-c', 'copy', '-map', '0', '-dn'] options = [
'-c', 'copy', '-map', '0', '-dn', '-attach', thumbnail_filename,
mimetype = 'image/%s' % ('png' if thumbnail_ext == 'png' else 'jpeg') '-metadata:s:t', 'mimetype=image/jpeg', '-metadata:s:t', 'filename=cover.jpg']
old_stream, new_stream = self.get_stream_number(
filename, ('tags', 'mimetype'), mimetype)
if old_stream is not None:
options.extend(['-map', '-0:%d' % old_stream])
new_stream -= 1
options.extend([
'-attach', thumbnail_filename,
'-metadata:s:%d' % new_stream, 'mimetype=%s' % mimetype,
'-metadata:s:%d' % new_stream, 'filename=cover.%s' % thumbnail_ext])
self.to_screen('Adding thumbnail to "%s"' % filename) self.to_screen('Adding thumbnail to "%s"' % filename)
self.run_ffmpeg(filename, temp_filename, options) self.run_ffmpeg_multiple_files([filename], temp_filename, options)
elif info['ext'] in ['m4a', 'mp4', 'mov']: elif info['ext'] in ['m4a', 'mp4']:
try: if not check_executable('AtomicParsley', ['-v']):
options = ['-c', 'copy', '-map', '0', '-dn', '-map', '1'] raise EmbedThumbnailPPError('AtomicParsley was not found. Please install.')
old_stream, new_stream = self.get_stream_number( cmd = [encodeFilename('AtomicParsley', True),
filename, ('disposition', 'attached_pic'), 1) encodeFilename(filename, True),
if old_stream is not None: encodeArgument('--artwork'),
options.extend(['-map', '-0:%d' % old_stream]) encodeFilename(thumbnail_filename, True),
new_stream -= 1 encodeArgument('-o'),
options.extend(['-disposition:%s' % new_stream, 'attached_pic']) encodeFilename(temp_filename, True)]
cmd += [encodeArgument(o) for o in self._configuration_args(exe='AtomicParsley')]
self.to_screen('Adding thumbnail to "%s"' % filename) self.to_screen('Adding thumbnail to "%s"' % filename)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
except PostProcessingError as err: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.report_warning('unable to embed using ffprobe & ffmpeg; %s' % error_to_compat_str(err)) stdout, stderr = process_communicate_or_kill(p)
if not check_executable('AtomicParsley', ['-v']):
raise EmbedThumbnailPPError('AtomicParsley was not found. Please install.')
cmd = [encodeFilename('AtomicParsley', True), if p.returncode != 0:
encodeFilename(filename, True), msg = stderr.decode('utf-8', 'replace').strip()
encodeArgument('--artwork'), raise EmbedThumbnailPPError(msg)
encodeFilename(thumbnail_filename, True), # for formats that don't support thumbnails (like 3gp) AtomicParsley
encodeArgument('-o'), # won't create to the temporary file
encodeFilename(temp_filename, True)] if b'No changes' in stdout:
cmd += [encodeArgument(o) for o in self._configuration_args(exe='AtomicParsley')] self.report_warning('The file format doesn\'t support embedding a thumbnail')
success = False
self.to_screen('Adding thumbnail to "%s"' % filename)
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process_communicate_or_kill(p)
if p.returncode != 0:
msg = stderr.decode('utf-8', 'replace').strip()
raise EmbedThumbnailPPError(msg)
# for formats that don't support thumbnails (like 3gp) AtomicParsley
# won't create to the temporary file
if b'No changes' in stdout:
self.report_warning('The file format doesn\'t support embedding a thumbnail')
success = False
elif info['ext'] in ['ogg', 'opus']:
if not _has_mutagen:
raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`')
size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]'
size_result = self.run_ffmpeg(thumbnail_filename, thumbnail_filename, ['-hide_banner'])
mobj = re.search(size_regex, size_result)
width, height = int(mobj.group('w')), int(mobj.group('h'))
mimetype = ('image/%s' % ('png' if thumbnail_ext == 'png' else 'jpeg')).encode('ascii')
# https://xiph.org/flac/format.html#metadata_block_picture
data = bytearray()
data += struct.pack('>II', 3, len(mimetype))
data += mimetype
data += struct.pack('>IIIIII', 0, width, height, 8, 0, os.stat(thumbnail_filename).st_size) # 32 if png else 24
fin = open(thumbnail_filename, "rb")
data += fin.read()
fin.close()
temp_filename = filename
f = mutagen.File(temp_filename)
f.tags['METADATA_BLOCK_PICTURE'] = base64.b64encode(data).decode('ascii')
f.save()
else: else:
raise EmbedThumbnailPPError('Supported filetypes for thumbnail embedding are: mp3, mkv/mka, ogg/opus, m4a/mp4/mov') raise EmbedThumbnailPPError('Only mp3, mkv, m4a and mp4 are supported for thumbnail embedding for now.')
if success and temp_filename != filename: if success:
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] files_to_delete = [] if self._already_have_thumbnail else [thumbnail_filename]
return files_to_delete, info return files_to_delete, info
+21 -87
View File
@@ -5,7 +5,6 @@ import os
import subprocess import subprocess
import time import time
import re import re
import json
from .common import AudioConversionError, PostProcessor from .common import AudioConversionError, PostProcessor
@@ -21,9 +20,8 @@ from ..utils import (
subtitles_filename, subtitles_filename,
dfxp2srt, dfxp2srt,
ISO639Utils, ISO639Utils,
process_communicate_or_kill,
replace_extension, replace_extension,
traverse_dict, process_communicate_or_kill,
) )
@@ -61,7 +59,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 or avconv not found. Please install one.')
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(
@@ -104,7 +102,7 @@ class FFmpegPostProcessor(PostProcessor):
if not os.path.exists(location): if not os.path.exists(location):
self.report_warning( self.report_warning(
'ffmpeg-location %s does not exist! ' 'ffmpeg-location %s does not exist! '
'Continuing without ffmpeg.' % (location)) 'Continuing without avconv/ffmpeg.' % (location))
self._versions = {} self._versions = {}
return return
elif not os.path.isdir(location): elif not os.path.isdir(location):
@@ -112,7 +110,7 @@ class FFmpegPostProcessor(PostProcessor):
if basename not in programs: if basename not in programs:
self.report_warning( self.report_warning(
'Cannot identify executable %s, its basename should be one of %s. ' 'Cannot identify executable %s, its basename should be one of %s. '
'Continuing without ffmpeg.' % 'Continuing without avconv/ffmpeg.' %
(location, ', '.join(programs))) (location, ', '.join(programs)))
self._versions = {} self._versions = {}
return None return None
@@ -165,7 +163,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/avprobe and ffmpeg/avconv not found. Please install one.')
try: try:
if self.probe_available: if self.probe_available:
cmd = [ cmd = [
@@ -203,37 +201,6 @@ class FFmpegPostProcessor(PostProcessor):
return mobj.group(1) return mobj.group(1)
return None return None
def get_metadata_object(self, path, opts=[]):
if self.probe_basename != 'ffprobe':
if self.probe_available:
self.report_warning('Only ffprobe is supported for metadata extraction')
raise PostProcessingError('ffprobe not found. Please install.')
self.check_version()
cmd = [
encodeFilename(self.probe_executable, True),
encodeArgument('-hide_banner'),
encodeArgument('-show_format'),
encodeArgument('-show_streams'),
encodeArgument('-print_format'),
encodeArgument('json'),
]
cmd += opts
cmd.append(encodeFilename(self._ffmpeg_filename_argument(path), True))
if self._downloader.params.get('verbose', False):
self._downloader.to_screen('[debug] ffprobe command line: %s' % shell_quote(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
stdout, stderr = p.communicate()
return json.loads(stdout.decode('utf-8', 'replace'))
def get_stream_number(self, path, keys, value):
streams = self.get_metadata_object(path)['streams']
num = next(
(i for i, stream in enumerate(streams) if traverse_dict(stream, keys, casesense=False) == value),
None)
return num, len(streams)
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
self.check_version() self.check_version()
@@ -260,23 +227,19 @@ class FFmpegPostProcessor(PostProcessor):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
stdout, stderr = process_communicate_or_kill(p) stdout, stderr = process_communicate_or_kill(p)
if p.returncode != 0: if p.returncode != 0:
stderr = stderr.decode('utf-8', 'replace').strip() stderr = stderr.decode('utf-8', 'replace')
if self._downloader.params.get('verbose', False): msg = stderr.strip().split('\n')[-1]
self.report_error(stderr) raise FFmpegPostProcessorError(msg)
raise FFmpegPostProcessorError(stderr.split('\n')[-1])
self.try_utime(out_path, oldest_mtime, oldest_mtime) self.try_utime(out_path, oldest_mtime, oldest_mtime)
return stderr.decode('utf-8', 'replace')
def run_ffmpeg(self, path, out_path, opts): def run_ffmpeg(self, path, out_path, opts):
return self.run_ffmpeg_multiple_files([path], out_path, opts) self.run_ffmpeg_multiple_files([path], out_path, opts)
def _ffmpeg_filename_argument(self, fn): def _ffmpeg_filename_argument(self, fn):
# Always use 'file:' because the filename may contain ':' (ffmpeg # Always use 'file:' because the filename may contain ':' (ffmpeg
# interprets that as a protocol) or can start with '-' (-- is broken in # interprets that as a protocol) or can start with '-' (-- is broken in
# ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details) # ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details)
# Also leave '-' intact in order not to break streaming to stdout. # Also leave '-' intact in order not to break streaming to stdout.
if fn.startswith(('http://', 'https://')):
return fn
return 'file:' + fn if fn != '-' else fn return 'file:' + fn if fn != '-' else fn
@@ -386,35 +349,21 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
class FFmpegVideoRemuxerPP(FFmpegPostProcessor): class FFmpegVideoRemuxerPP(FFmpegPostProcessor):
def __init__(self, downloader=None, preferedformat=None): def __init__(self, downloader=None, preferedformat=None):
super(FFmpegVideoRemuxerPP, self).__init__(downloader) super(FFmpegVideoRemuxerPP, self).__init__(downloader)
self._preferedformats = preferedformat.lower().split('/') self._preferedformat = preferedformat
def run(self, information): def run(self, information):
path = information['filepath'] path = information['filepath']
sourceext, targetext = information['ext'].lower(), None if information['ext'] == self._preferedformat:
for pair in self._preferedformats: self.to_screen('Not remuxing video file %s - already is in target format %s' % (path, self._preferedformat))
kv = pair.split('>')
if len(kv) == 1 or kv[0].strip() == sourceext:
targetext = kv[-1].strip()
break
_skip_msg = (
'could not find a mapping for %s' if not targetext
else 'already is in target format %s' if sourceext == targetext
else None)
if _skip_msg:
self.to_screen('Not remuxing media file %s; %s' % (path, _skip_msg % sourceext))
return [], information return [], information
options = ['-c', 'copy', '-map', '0', '-dn'] options = ['-c', 'copy', '-map', '0', '-dn']
if targetext in ['mp4', 'm4a', 'mov']: prefix, sep, ext = path.rpartition('.')
options.extend(['-movflags', '+faststart']) outpath = prefix + sep + self._preferedformat
prefix, sep, oldext = path.rpartition('.') self.to_screen('Remuxing video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
outpath = prefix + sep + targetext
self.to_screen('Remuxing video from %s to %s; Destination: %s' % (sourceext, targetext, outpath))
self.run_ffmpeg(path, outpath, options) self.run_ffmpeg(path, outpath, options)
information['filepath'] = outpath information['filepath'] = outpath
information['format'] = targetext information['format'] = self._preferedformat
information['ext'] = targetext information['ext'] = self._preferedformat
return [path], information return [path], information
@@ -457,22 +406,18 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
sub_langs = [] sub_langs = []
sub_filenames = [] sub_filenames = []
webm_vtt_warn = False webm_vtt_warn = False
mp4_ass_warn = False
for lang, sub_info in subtitles.items(): for lang, sub_info in subtitles.items():
sub_ext = sub_info['ext'] sub_ext = sub_info['ext']
if sub_ext == 'json': if sub_ext == 'json':
self.report_warning('JSON subtitles cannot be embedded') self.to_screen('JSON subtitles cannot be embedded')
elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt': elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
sub_langs.append(lang) sub_langs.append(lang)
sub_filenames.append(subtitles_filename(filename, lang, sub_ext, ext)) sub_filenames.append(subtitles_filename(filename, lang, sub_ext, ext))
else: else:
if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt': if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt':
webm_vtt_warn = True webm_vtt_warn = True
self.report_warning('Only WebVTT subtitles can be embedded in webm files') self.to_screen('Only WebVTT subtitles can be embedded in webm files')
if not mp4_ass_warn and ext == 'mp4' and sub_ext == 'ass':
mp4_ass_warn = True
self.report_warning('ASS subtitles cannot be properly embedded in mp4 files; expect issues')
if not sub_langs: if not sub_langs:
return [], information return [], information
@@ -496,7 +441,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code]) opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
temp_filename = prepend_extension(filename, 'temp') temp_filename = prepend_extension(filename, 'temp')
self.to_screen('Embedding subtitles in "%s"' % filename) self.to_screen('Embedding subtitles in \'%s\'' % filename)
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
os.remove(encodeFilename(filename)) os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename))
@@ -526,6 +471,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
# 1. https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/ # 1. https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
# 2. https://wiki.multimedia.cx/index.php/FFmpeg_Metadata # 2. https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
# 3. https://kodi.wiki/view/Video_file_tagging # 3. https://kodi.wiki/view/Video_file_tagging
# 4. http://atomicparsley.sourceforge.net/mpeg-4files.html
add('title', ('track', 'title')) add('title', ('track', 'title'))
add('date', 'upload_date') add('date', 'upload_date')
@@ -578,18 +524,6 @@ 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'):
old_stream, new_stream = self.get_stream_number(
filename, ('tags', 'mimetype'), 'application/json')
if old_stream is not None:
options.extend(['-map', '-0:%d' % old_stream])
new_stream -= 1
options.extend([
'-attach', info['__infojson_filepath'],
'-metadata:s:%d' % new_stream, 'mimetype=application/json'
])
self.to_screen('Adding metadata to \'%s\'' % filename) self.to_screen('Adding metadata to \'%s\'' % filename)
self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options) self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options)
if chapters: if chapters:
@@ -1,66 +0,0 @@
from __future__ import unicode_literals
import re
from .common import PostProcessor
from ..compat import compat_str
class MetadataFromFieldPP(PostProcessor):
regex = r'(?P<field>\w+):(?P<format>.+)$'
def __init__(self, downloader, formats):
PostProcessor.__init__(self, downloader)
assert isinstance(formats, (list, tuple))
self._data = []
for f in formats:
assert isinstance(f, compat_str)
match = re.match(self.regex, f)
assert match is not None
self._data.append({
'field': match.group('field'),
'format': match.group('format'),
'regex': self.format_to_regex(match.group('format'))})
def format_to_regex(self, fmt):
r"""
Converts a string like
'%(title)s - %(artist)s'
to a regex like
'(?P<title>.+)\ \-\ (?P<artist>.+)'
"""
if not re.search(r'%\(\w+\)s', fmt):
return fmt
lastpos = 0
regex = ''
# replace %(..)s with regex group and escape other string parts
for match in re.finditer(r'%\((\w+)\)s', fmt):
regex += re.escape(fmt[lastpos:match.start()])
regex += r'(?P<' + match.group(1) + r'>[^\r\n]+)'
lastpos = match.end()
if lastpos < len(fmt):
regex += re.escape(fmt[lastpos:])
return regex
def run(self, info):
for dictn in self._data:
field, regex = dictn['field'], dictn['regex']
if field not in info:
self.report_warning('Video doesnot have a %s' % field)
continue
self.write_debug('Searching for r"%s" in %s' % (regex, field))
match = re.search(regex, info[field])
if match is None:
self.report_warning('Could not interpret video %s as "%s"' % (field, dictn['format']))
continue
for attribute, value in match.groupdict().items():
info[attribute] = value
self.to_screen('parsed %s from %s: %s' % (attribute, field, value if value is not None else 'NA'))
return [], info
class MetadataFromTitlePP(MetadataFromFieldPP): # for backward compatibility
def __init__(self, downloader, titleformat):
super(MetadataFromTitlePP, self).__init__(downloader, ['title:%s' % titleformat])
self._titleformat = titleformat
self._titleregex = self._data[0]['regex']
@@ -0,0 +1,44 @@
from __future__ import unicode_literals
import re
from .common import PostProcessor
class MetadataFromTitlePP(PostProcessor):
def __init__(self, downloader, titleformat):
super(MetadataFromTitlePP, self).__init__(downloader)
self._titleformat = titleformat
self._titleregex = (self.format_to_regex(titleformat)
if re.search(r'%\(\w+\)s', titleformat)
else titleformat)
def format_to_regex(self, fmt):
r"""
Converts a string like
'%(title)s - %(artist)s'
to a regex like
'(?P<title>.+)\ \-\ (?P<artist>.+)'
"""
lastpos = 0
regex = ''
# replace %(..)s with regex group and escape other string parts
for match in re.finditer(r'%\((\w+)\)s', fmt):
regex += re.escape(fmt[lastpos:match.start()])
regex += r'(?P<' + match.group(1) + '>.+)'
lastpos = match.end()
if lastpos < len(fmt):
regex += re.escape(fmt[lastpos:])
return regex
def run(self, info):
title = info['title']
match = re.match(self._titleregex, title)
if match is None:
self.to_screen('Could not interpret title of video as "%s"' % self._titleformat)
return [], info
for attribute, value in match.groupdict().items():
info[attribute] = value
self.to_screen('parsed %s: %s' % (attribute, value if value is not None else 'NA'))
return [], info
@@ -28,14 +28,14 @@ class MoveFilesAfterDownloadPP(PostProcessor):
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():
if not os.path.exists(encodeFilename(oldfile)):
self.report_warning('File "%s" cannot be found' % oldfile)
continue
if not newfile: if not newfile:
newfile = os.path.join(finaldir, os.path.basename(encodeFilename(oldfile))) newfile = os.path.join(finaldir, os.path.basename(encodeFilename(oldfile)))
oldfile, newfile = compat_str(oldfile), compat_str(newfile) oldfile, newfile = compat_str(oldfile), compat_str(newfile)
if os.path.abspath(encodeFilename(oldfile)) == os.path.abspath(encodeFilename(newfile)): if os.path.abspath(encodeFilename(oldfile)) == os.path.abspath(encodeFilename(newfile)):
continue continue
if not os.path.exists(encodeFilename(oldfile)):
self.report_warning('File "%s" cannot be found' % oldfile)
continue
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)
-11
View File
@@ -5934,14 +5934,3 @@ def load_plugins(name, type, namespace):
if plugin_info[0] is not None: if plugin_info[0] is not None:
plugin_info[0].close() plugin_info[0].close()
return classes return classes
def traverse_dict(dictn, keys, casesense=True):
if not isinstance(dictn, dict):
return None
first_key = keys[0]
if not casesense:
dictn = {key.lower(): val for key, val in dictn.items()}
first_key = first_key.lower()
value = dictn.get(first_key, None)
return value if len(keys) < 2 else traverse_dict(value, keys[1:], casesense)