Compare commits
10 Commits
db302b339d
...
3bef839e18
| Author | SHA1 | Date |
|---|---|---|
|
|
3bef839e18 | |
|
|
1b32d49fcf | |
|
|
c4d7dd9948 | |
|
|
ecfc188388 | |
|
|
916ed330dd | |
|
|
136c722636 | |
|
|
588119d9eb | |
|
|
adf341f857 | |
|
|
27aa865c37 | |
|
|
d19ca4e87a |
|
|
@ -13,7 +13,7 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing discussions and issues for similar configuration problems
|
- label: I have searched existing discussions and issues for similar configuration problems
|
||||||
required: true
|
required: true
|
||||||
- label: I have read the [configuration section](https://github.com/alexta69/metube#configuration-via-environment-variables) in the README
|
- label: I have read the [configuration section](https://github.com/alexta69/metube#%EF%B8%8F-configuration-via-environment-variables) in the README
|
||||||
required: true
|
required: true
|
||||||
- label: I have checked the [Wiki](https://github.com/alexta69/metube/wiki) for configuration examples
|
- label: I have checked the [Wiki](https://github.com/alexta69/metube/wiki) for configuration examples
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -24,7 +24,7 @@ body:
|
||||||
## Configuration Resources
|
## Configuration Resources
|
||||||
|
|
||||||
Before asking for help, please check these resources:
|
Before asking for help, please check these resources:
|
||||||
- **[Configuration Guide](https://github.com/alexta69/metube#configuration-via-environment-variables)** - All available environment variables
|
- **[Configuration Guide](https://github.com/alexta69/metube#%EF%B8%8F-configuration-via-environment-variables)** - All available environment variables
|
||||||
- **[YTDL_OPTIONS Cookbook](https://github.com/alexta69/metube/wiki/YTDL_OPTIONS-Cookbook)** - Common yt-dlp configurations
|
- **[YTDL_OPTIONS Cookbook](https://github.com/alexta69/metube/wiki/YTDL_OPTIONS-Cookbook)** - Common yt-dlp configurations
|
||||||
- **[OUTPUT_TEMPLATE Cookbook](https://github.com/alexta69/metube/wiki/OUTPUT_TEMPLATE-Cookbook)** - Filename template examples
|
- **[OUTPUT_TEMPLATE Cookbook](https://github.com/alexta69/metube/wiki/OUTPUT_TEMPLATE-Cookbook)** - Filename template examples
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing issues and discussions to ensure this bug hasn't been reported before
|
- label: I have searched existing issues and discussions to ensure this bug hasn't been reported before
|
||||||
required: true
|
required: true
|
||||||
- label: I have read the [troubleshooting section](https://github.com/alexta69/metube#troubleshooting-and-submitting-issues) in the README
|
- label: I have read the [troubleshooting section](https://github.com/alexta69/metube#-troubleshooting-and-submitting-issues) in the README
|
||||||
required: true
|
required: true
|
||||||
- label: I have tested this issue with yt-dlp directly (not just through MeTube UI) as described in the README
|
- label: I have tested this issue with yt-dlp directly (not just through MeTube UI) as described in the README
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: I have searched existing issues and discussions to ensure this feature hasn't been requested before
|
- label: I have searched existing issues and discussions to ensure this feature hasn't been requested before
|
||||||
required: true
|
required: true
|
||||||
- label: I have read the [feature request guidelines](https://github.com/alexta69/metube#submitting-feature-requests) in the README
|
- label: I have read the [feature request guidelines](https://github.com/alexta69/metube#-submitting-feature-requests) in the README
|
||||||
required: true
|
required: true
|
||||||
- label: I understand that MeTube development relies on community contributions and the maintainer is not likely to implement this feature
|
- label: I understand that MeTube development relies on community contributions and the maintainer is not likely to implement this feature
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I have checked the [Wiki](https://github.com/alexta69/metube/wiki) for configuration examples
|
- label: I have checked the [Wiki](https://github.com/alexta69/metube/wiki) for configuration examples
|
||||||
required: true
|
required: true
|
||||||
- label: I have read the [troubleshooting section](https://github.com/alexta69/metube#troubleshooting-and-submitting-issues) if this is a technical issue
|
- label: I have read the [troubleshooting section](https://github.com/alexta69/metube#-troubleshooting-and-submitting-issues) if this is a technical issue
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|
|
||||||
79
README.md
79
README.md
|
|
@ -33,55 +33,55 @@ Certain values can be set via environment variables, using the `-e` parameter on
|
||||||
|
|
||||||
### ⬇️ Download Behavior
|
### ⬇️ Download Behavior
|
||||||
|
|
||||||
* __DOWNLOAD_MODE__ :This flag controls how downloads are scheduled and executed. Options are `sequential`, `concurrent`, and `limited`. Defaults to `limited`:
|
* __DOWNLOAD_MODE__: This flag controls how downloads are scheduled and executed. Options are `sequential`, `concurrent`, and `limited`. Defaults to `limited`:
|
||||||
* `sequential`: Downloads are processed one at a time. A new download won't start until the previous one has finished. This mode is useful for conserving system resources or ensuring downloads occur in a strict order.
|
* `sequential`: Downloads are processed one at a time. A new download won't start until the previous one has finished. This mode is useful for conserving system resources or ensuring downloads occur in strict order.
|
||||||
* `concurrent`: Downloads are started immediately as they are added, with no built-in limit on how many run simultaneously. This mode may overwhelm your system if too many downloads start at once.
|
* `concurrent`: Downloads are started immediately as they are added, with no built-in limit on how many run simultaneously. This mode may overwhelm your system if too many downloads start at once.
|
||||||
* `limited`: Downloads are started concurrently but are capped by a concurrency limit. In this mode, a semaphore is used so that at most a fixed number of downloads run at any given time.
|
* `limited`: Downloads are started concurrently but are capped by a concurrency limit. In this mode, a semaphore is used so that at most a fixed number of downloads run at any given time.
|
||||||
* **MAX\_CONCURRENT\_DOWNLOADS** This flag is used only when **DOWNLOAD\_MODE** is set to **limited**.
|
* __MAX_CONCURRENT_DOWNLOADS__: This flag is used only when `DOWNLOAD_MODE` is set to `limited`.
|
||||||
It specifies the maximum number of simultaneous downloads allowed. For example, if set to `5`, then at most five downloads will run concurrently, and any additional downloads will wait until one of the active downloads completes. Defaults to `3`.
|
It specifies the maximum number of simultaneous downloads allowed. For example, if set to `5`, then at most five downloads will run concurrently, and any additional downloads will wait until one of the active downloads completes. Defaults to `3`.
|
||||||
* __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`.
|
* __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`.
|
||||||
* __DEFAULT_OPTION_PLAYLIST_STRICT_MODE__: if `true`, the "Strict Playlist mode" switch will be enabled by default. In this mode the playlists will be downloaded only if the url strictly points to a playlist. Urls to videos inside a playlist will be treated same as direct video url. Defaults to `false` .
|
* __DEFAULT_OPTION_PLAYLIST_STRICT_MODE__: if `true`, the "Strict Playlist mode" switch will be enabled by default. In this mode the playlists will be downloaded only if the URL strictly points to a playlist. URLs to videos inside a playlist will be treated same as direct video URL. Defaults to `false` .
|
||||||
* __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum number of playlist items that can be downloaded. Defaults to `0` (no limit).
|
* __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum number of playlist items that can be downloaded. Defaults to `0` (no limit).
|
||||||
|
|
||||||
### 📁 Storage & Directories
|
### 📁 Storage & Directories
|
||||||
|
|
||||||
* __DOWNLOAD_DIR__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise.
|
* __DOWNLOAD_DIR__: Path to where the downloads will be saved. Defaults to `/downloads` in the Docker image, and `.` otherwise.
|
||||||
* __AUDIO_DOWNLOAD_DIR__: path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`.
|
* __AUDIO_DOWNLOAD_DIR__: Path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`.
|
||||||
* __CUSTOM_DIRS__: whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a drop-down appears next to the Add button to specify the download directory. Defaults to `true`.
|
* __CUSTOM_DIRS__: Whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a dropdown appears next to the Add button to specify the download directory. Defaults to `true`.
|
||||||
* __CREATE_CUSTOM_DIRS__: whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector becomes supports free-text input, and the specified directory will be created recursively. Defaults to `true`.
|
* __CREATE_CUSTOM_DIRS__: Whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector supports free-text input, and the specified directory will be created recursively. Defaults to `true`.
|
||||||
* __CUSTOM_DIRS_EXCLUDE_REGEX__: regular expression to exclude some custom directories from the drop-down. Empty regex disables exclusion. Defaults to `(^|/)[.@].*$`, which means directories starting with `.` or `@`.
|
* __CUSTOM_DIRS_EXCLUDE_REGEX__: Regular expression to exclude some custom directories from the dropdown. Empty regex disables exclusion. Defaults to `(^|/)[.@].*$`, which means directories starting with `.` or `@`.
|
||||||
* __DOWNLOAD_DIRS_INDEXABLE__: if `true`, the download dirs (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the webserver. Defaults to `false`.
|
* __DOWNLOAD_DIRS_INDEXABLE__: If `true`, the download directories (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the web server. Defaults to `false`.
|
||||||
* __STATE_DIR__: path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise.
|
* __STATE_DIR__: Path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the Docker image, and `.` otherwise.
|
||||||
* __TEMP_DIR__: path where intermediary download files will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise.
|
* __TEMP_DIR__: Path where intermediary download files will be saved. Defaults to `/downloads` in the Docker image, and `.` otherwise.
|
||||||
* Set this to an SSD or RAM filesystem (e.g., `tmpfs`) for better performance
|
* Set this to an SSD or RAM filesystem (e.g., `tmpfs`) for better performance.
|
||||||
* __Note__: Using a RAM filesystem may prevent downloads from being resumed
|
* __Note__: Using a RAM filesystem may prevent downloads from being resumed.
|
||||||
|
|
||||||
### 🌐 Web Server & URLs
|
|
||||||
|
|
||||||
* __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`.
|
|
||||||
* __PUBLIC_HOST_URL__: base URL for the download links shown in the UI for completed files. By default MeTube serves them under its own URL. If your download directory is accessible on another URL and you want the download links to be based there, use this variable to set it.
|
|
||||||
* __PUBLIC_HOST_AUDIO_URL__: same as PUBLIC_HOST_URL but for audio downloads.
|
|
||||||
* __HTTPS__: use `https` instead of `http`(__CERTFILE__ and __KEYFILE__ required). Defaults to `false`.
|
|
||||||
* __CERTFILE__: HTTPS certificate file path.
|
|
||||||
* __KEYFILE__: HTTPS key file path.
|
|
||||||
|
|
||||||
### 📝 File Naming & yt-dlp
|
### 📝 File Naming & yt-dlp
|
||||||
|
|
||||||
* __OUTPUT_TEMPLATE__: the template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`.
|
* __OUTPUT_TEMPLATE__: The template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`.
|
||||||
* __OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`.
|
* __OUTPUT_TEMPLATE_CHAPTER__: The template for the filenames of the downloaded videos when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`.
|
||||||
* __OUTPUT_TEMPLATE_PLAYLIST__: the template for the filenames of the downloaded videos, when downloaded as a playlist. Defaults to `%(playlist_title)s/%(title)s.%(ext)s`. When empty then `OUTPUT_TEMPLATE` is used.
|
* __OUTPUT_TEMPLATE_PLAYLIST__: The template for the filenames of the downloaded videos when downloaded as a playlist. Defaults to `%(playlist_title)s/%(title)s.%(ext)s`. When empty, then `OUTPUT_TEMPLATE` is used.
|
||||||
* __YTDL_OPTIONS__: Additional options to pass to yt-dlp, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L220). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command line options to `YTDL_OPTIONS`.
|
* __YTDL_OPTIONS__: Additional options to pass to yt-dlp in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L222). They roughly correspond to command-line options, though some do not have exact equivalents here. For example, `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command-line options to `YTDL_OPTIONS`.
|
||||||
* __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence.
|
* __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. The file will be monitored for changes and reloaded automatically when changes are detected.
|
||||||
|
|
||||||
|
### 🌐 Web Server & URLs
|
||||||
|
|
||||||
|
* __URL_PREFIX__: Base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`.
|
||||||
|
* __PUBLIC_HOST_URL__: Base URL for the download links shown in the UI for completed files. By default, MeTube serves them under its own URL. If your download directory is accessible on another URL and you want the download links to be based there, use this variable to set it.
|
||||||
|
* __PUBLIC_HOST_AUDIO_URL__: Same as PUBLIC_HOST_URL but for audio downloads.
|
||||||
|
* __HTTPS__: Use `https` instead of `http` (__CERTFILE__ and __KEYFILE__ required). Defaults to `false`.
|
||||||
|
* __CERTFILE__: HTTPS certificate file path.
|
||||||
|
* __KEYFILE__: HTTPS key file path.
|
||||||
|
* __ROBOTS_TXT__: A path to a `robots.txt` file mounted in the container.
|
||||||
|
|
||||||
### 🏠 Basic Setup
|
### 🏠 Basic Setup
|
||||||
|
|
||||||
* __UID__: user under which MeTube will run. Defaults to `1000`.
|
* __UID__: User under which MeTube will run. Defaults to `1000`.
|
||||||
* __GID__: group under which MeTube will run. Defaults to `1000`.
|
* __GID__: Group under which MeTube will run. Defaults to `1000`.
|
||||||
* __UMASK__: umask value used by MeTube. Defaults to `022`.
|
* __UMASK__: Umask value used by MeTube. Defaults to `022`.
|
||||||
* __DEFAULT_THEME__: default theme to use for the ui, can be set to `light`, `dark` or `auto`. Defaults to `auto`.
|
* __DEFAULT_THEME__: Default theme to use for the UI, can be set to `light`, `dark`, or `auto`. Defaults to `auto`.
|
||||||
* __LOGLEVEL__: Log level, can be set to `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` or `NONE`. Defaults to `INFO`.
|
* __LOGLEVEL__: Log level, can be set to `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`, or `NONE`. Defaults to `INFO`.
|
||||||
* __ENABLE_ACCESSLOG__: whether to enable access log. Defaults to `false`.
|
* __ENABLE_ACCESSLOG__: Whether to enable access log. Defaults to `false`.
|
||||||
* __ROBOTS_TXT__: A path to a `robots.txt` file mounted in the container
|
|
||||||
|
|
||||||
The project's Wiki contains examples of useful configurations contributed by users of MeTube:
|
The project's Wiki contains examples of useful configurations contributed by users of MeTube:
|
||||||
* [YTDL_OPTIONS Cookbook](https://github.com/alexta69/metube/wiki/YTDL_OPTIONS-Cookbook)
|
* [YTDL_OPTIONS Cookbook](https://github.com/alexta69/metube/wiki/YTDL_OPTIONS-Cookbook)
|
||||||
|
|
@ -123,7 +123,7 @@ __Firefox:__ contributed by [nanocortex](https://github.com/nanocortex). You can
|
||||||
|
|
||||||
iOS has strict requirements for video files, requiring h264 or h265 video codec and aac audio codec in MP4 container. This can sometimes be a lower quality than the best quality available. To accommodate iOS requirements, when downloading a MP4 format you can choose "Best (iOS)" to get the best quality formats as compatible as possible with iOS requirements.
|
iOS has strict requirements for video files, requiring h264 or h265 video codec and aac audio codec in MP4 container. This can sometimes be a lower quality than the best quality available. To accommodate iOS requirements, when downloading a MP4 format you can choose "Best (iOS)" to get the best quality formats as compatible as possible with iOS requirements.
|
||||||
|
|
||||||
To force all downloads to be converted to an iOS compatible codec insert this as an environment variable
|
To force all downloads to be converted to an iOS-compatible codec, insert this as an environment variable:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -267,7 +267,7 @@ MeTube development relies on code contributions by the community. The program as
|
||||||
|
|
||||||
## 🛠️ Building and running locally
|
## 🛠️ Building and running locally
|
||||||
|
|
||||||
Make sure you have node.js and Python 3.13 installed.
|
Make sure you have Node.js and Python 3.13 installed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd metube/ui
|
cd metube/ui
|
||||||
|
|
@ -288,7 +288,4 @@ A Docker image can be built locally (it will build the UI too):
|
||||||
docker build -t metube .
|
docker build -t metube .
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 Development notes
|
Note that if you're running the server in VSCode, your downloads will go to your user's Downloads folder (this is configured via the environment in `.vscode/launch.json`).
|
||||||
|
|
||||||
* The above works on Windows and macOS as well as Linux.
|
|
||||||
* If you're running the server in VSCode, your downloads will go to your user's Downloads folder (this is configured via the environment in .vscode/launch.json).
|
|
||||||
|
|
|
||||||
14
app/main.py
14
app/main.py
|
|
@ -115,10 +115,18 @@ config = Config()
|
||||||
|
|
||||||
class ObjectSerializer(json.JSONEncoder):
|
class ObjectSerializer(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, object):
|
# First try to use __dict__ for custom objects
|
||||||
|
if hasattr(obj, '__dict__'):
|
||||||
return obj.__dict__
|
return obj.__dict__
|
||||||
else:
|
# Convert iterables (generators, dict_items, etc.) to lists
|
||||||
return json.JSONEncoder.default(self, obj)
|
# Exclude strings and bytes which are also iterable
|
||||||
|
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes)):
|
||||||
|
try:
|
||||||
|
return list(obj)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# Fall back to default behavior
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
serializer = ObjectSerializer()
|
serializer = ObjectSerializer()
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
|
|
|
||||||
59
app/ytdl.py
59
app/ytdl.py
|
|
@ -31,7 +31,7 @@ class DownloadQueueNotifier:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
class DownloadInfo:
|
class DownloadInfo:
|
||||||
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error):
|
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit):
|
||||||
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
|
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
|
||||||
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
|
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
@ -44,6 +44,8 @@ class DownloadInfo:
|
||||||
self.size = None
|
self.size = None
|
||||||
self.timestamp = time.time_ns()
|
self.timestamp = time.time_ns()
|
||||||
self.error = error
|
self.error = error
|
||||||
|
self.entry = entry
|
||||||
|
self.playlist_item_limit = playlist_item_limit
|
||||||
|
|
||||||
class Download:
|
class Download:
|
||||||
manager = None
|
manager = None
|
||||||
|
|
@ -240,11 +242,11 @@ class DownloadQueue:
|
||||||
|
|
||||||
async def __import_queue(self):
|
async def __import_queue(self):
|
||||||
for k, v in self.queue.saved_items():
|
for k, v in self.queue.saved_items():
|
||||||
await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix, getattr(v, 'playlist_strict_mode', False), getattr(v, 'playlist_item_limit', 0), getattr(v, 'auto_start', True))
|
await self.__add_download(v, True)
|
||||||
|
|
||||||
async def __import_pending(self):
|
async def __import_pending(self):
|
||||||
for k, v in self.pending.saved_items():
|
for k, v in self.pending.saved_items():
|
||||||
await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix, getattr(v, 'playlist_strict_mode', False), getattr(v, 'playlist_item_limit', 0), getattr(v, 'auto_start', False))
|
await self.__add_download(v, False)
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
log.info("Initializing DownloadQueue")
|
log.info("Initializing DownloadQueue")
|
||||||
|
|
@ -327,6 +329,32 @@ class DownloadQueue:
|
||||||
dldirectory = base_directory
|
dldirectory = base_directory
|
||||||
return dldirectory, None
|
return dldirectory, None
|
||||||
|
|
||||||
|
async def __add_download(self, dl, auto_start):
|
||||||
|
dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder)
|
||||||
|
if error_message is not None:
|
||||||
|
return error_message
|
||||||
|
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
||||||
|
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
|
||||||
|
entry = getattr(dl, 'entry', None)
|
||||||
|
if entry is not None and 'playlist' in entry and entry['playlist'] is not None:
|
||||||
|
if len(self.config.OUTPUT_TEMPLATE_PLAYLIST):
|
||||||
|
output = self.config.OUTPUT_TEMPLATE_PLAYLIST
|
||||||
|
for property, value in entry.items():
|
||||||
|
if property.startswith("playlist"):
|
||||||
|
output = output.replace(f"%({property})s", str(value))
|
||||||
|
ytdl_options = dict(self.config.YTDL_OPTIONS)
|
||||||
|
playlist_item_limit = getattr(dl, 'playlist_item_limit', 0)
|
||||||
|
if playlist_item_limit > 0:
|
||||||
|
log.info(f'playlist limit is set. Processing only first {playlist_item_limit} entries')
|
||||||
|
ytdl_options['playlistend'] = playlist_item_limit
|
||||||
|
download = Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, dl.quality, dl.format, ytdl_options, dl)
|
||||||
|
if auto_start is True:
|
||||||
|
self.queue.put(download)
|
||||||
|
asyncio.create_task(self.__start_download(download))
|
||||||
|
else:
|
||||||
|
self.pending.put(download)
|
||||||
|
await self.notifier.added(dl)
|
||||||
|
|
||||||
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already):
|
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already):
|
||||||
if not entry:
|
if not entry:
|
||||||
return {'status': 'error', 'msg': "Invalid/empty data was given."}
|
return {'status': 'error', 'msg': "Invalid/empty data was given."}
|
||||||
|
|
@ -368,29 +396,8 @@ class DownloadQueue:
|
||||||
log.debug('Processing as a video')
|
log.debug('Processing as a video')
|
||||||
key = entry.get('webpage_url') or entry['url']
|
key = entry.get('webpage_url') or entry['url']
|
||||||
if not self.queue.exists(key):
|
if not self.queue.exists(key):
|
||||||
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, error)
|
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit)
|
||||||
dldirectory, error_message = self.__calc_download_path(quality, format, folder)
|
await self.__add_download(dl, auto_start)
|
||||||
if error_message is not None:
|
|
||||||
return error_message
|
|
||||||
output = self.config.OUTPUT_TEMPLATE if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
|
||||||
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
|
|
||||||
if 'playlist' in entry and entry['playlist'] is not None:
|
|
||||||
if len(self.config.OUTPUT_TEMPLATE_PLAYLIST):
|
|
||||||
output = self.config.OUTPUT_TEMPLATE_PLAYLIST
|
|
||||||
for property, value in entry.items():
|
|
||||||
if property.startswith("playlist"):
|
|
||||||
output = output.replace(f"%({property})s", str(value))
|
|
||||||
ytdl_options = dict(self.config.YTDL_OPTIONS)
|
|
||||||
if playlist_item_limit > 0:
|
|
||||||
log.info(f'playlist limit is set. Processing only first {playlist_item_limit} entries')
|
|
||||||
ytdl_options['playlistend'] = playlist_item_limit
|
|
||||||
if auto_start is True:
|
|
||||||
download = Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl)
|
|
||||||
self.queue.put(download)
|
|
||||||
asyncio.create_task(self.__start_download(download))
|
|
||||||
else:
|
|
||||||
self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl))
|
|
||||||
await self.notifier.added(dl)
|
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
|
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
|
||||||
|
|
||||||
|
|
|
||||||
6
uv.lock
6
uv.lock
|
|
@ -744,11 +744,11 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yt-dlp"
|
name = "yt-dlp"
|
||||||
version = "2025.9.23"
|
version = "2025.10.22"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/90/77/24a13bbd3190849e7e37e6093aa9648f3b26a836d37ba67e3429ee89ad1d/yt_dlp-2025.9.23.tar.gz", hash = "sha256:9282ad1deadb4c90b2e6d3bcf9f360abf88c5f2e4ba836dad7b51387b086d757", size = 3036855, upload-time = "2025-09-23T06:54:46.174Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/08/70/cf4bd6c837ab0a709040888caa70d166aa2dfbb5018d1d5c983bf0b50254/yt_dlp-2025.10.22.tar.gz", hash = "sha256:db2d48133222b1d9508c6de757859c24b5cefb9568cf68ccad85dac20b07f77b", size = 3046863, upload-time = "2025-10-22T19:53:19.301Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/b7/723a4061d7efac4bbb86eddef9ceec0fc1e85415c7d665c2e5e1879759bb/yt_dlp-2025.9.23-py3-none-any.whl", hash = "sha256:84e36acf2dfbadb307d734a590dd937923173b13c57cf45a662f40c93e746302", size = 3241497, upload-time = "2025-09-23T06:54:43.485Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/2a/fd184bf97d570841aa86b4aeb84aee93e7957a34059dafd4982157c10bff/yt_dlp-2025.10.22-py3-none-any.whl", hash = "sha256:9c803a9598859f91d0d5bd3337f1506ecb40bbe97f6efbe93bc4461fed344fb2", size = 3248983, upload-time = "2025-10-22T19:53:16.483Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue