From f8553c7ffca1f96692ff54cea76e21455dd97199 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 30 Apr 2025 17:04:20 -0400 Subject: [PATCH 001/103] Add Proof-of-Origin Token plugin framework --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 2976db2e..17288683 100644 --- a/Pipfile +++ b/Pipfile @@ -24,3 +24,4 @@ yt-dlp = {extras = ["default", "curl-cffi"], version = "*"} emoji = "*" brotli = "*" html5lib = "*" +yt-dlp-get-pot = "*" From 42456f6389a74e1c0b8df5f9929f766aa408664c Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 04:21:33 -0400 Subject: [PATCH 002/103] Update release.yaml --- .github/workflows/release.yaml | 88 ++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9e2992d7..dea6c23f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,31 +8,89 @@ on: types: [published] jobs: - containerise: + info: runs-on: ubuntu-latest + outputs: + ffmpeg-date: ${{ steps.jq.outputs.FFMPEG_DATE }} + ffmpeg-releases: ${{ steps.ffmpeg.outputs.releases }} + ffmpeg-version: ${{ steps.jq.outputs.FFMPEG_VERSION }} + lowercase-github-actor: ${{ steps.github-actor.outputs.lowercase }} + lowercase-github-repository_owner: ${{ steps.github-repository_owner.outputs.lowercase }} + tag: ${{ steps.tag.outputs.tag }} + ytdlp-latest-release: ${{ steps.yt-dlp.outputs.latest-release }} + ytdlp-releases: ${{ steps.yt-dlp.outputs.releases }} steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - name: Get tag id: tag uses: dawidd6/action-get-tag@v1 - - uses: docker/build-push-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Log into GitHub Container Registry - run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin - - name: Lowercase github username for ghcr - id: string - uses: ASzc/change-string-case-action@v1 + - uses: actions/checkout@v4 + - name: Lowercase github username + id: github-actor + uses: ./.github/actions/string-case with: string: ${{ github.actor }} + - name: Lowercase github repository owner + id: github-repository_owner + uses: ./.github/actions/string-case + with: + string: ${{ github.repository_owner }} + - name: Retrieve yt-dlp/FFmpeg-Builds releases with GitHub CLI + id: ffmpeg + uses: ./.github/actions/FFmpeg + - name: Retrieve yt-dlp/yt-dlp releases with GitHub CLI + id: yt-dlp + uses: ./.github/actions/yt-dlp + - name: Set outputs with jq + id: jq + run: | + cat >| .ffmpeg.releases.json <<'EOF' + ${{ steps.ffmpeg.outputs.releases }} + EOF + mk_delim() { local f='%s_EOF_%d_' ; printf -- "${f}" "$1" "${RANDOM}" ; } ; + open_ml_var() { local f=''\%'s<<'\%'s\n' ; printf -- "${f}" "$2" "$1" ; } ; + close_ml_var() { local f='%s\n' ; printf -- "${f}" "$1" ; } ; + { + var='FFMPEG_DATE' ; + delim="$(mk_delim "${var}")" ; + open_ml_var "${delim}" "${var}" ; + jq_arg='[foreach .[] as $release ([{}, []]; [ .[0] + {($release.commit): ([ $release.date ] + (.[0][($release.commit)] // []) ) }, [ .[1][0] // $release.commit ] ] ; .[0][(.[1][0])] ) ][-1][0]' ; + jq -r "${jq_arg}" -- .ffmpeg.releases.json ; + close_ml_var "${delim}" "${var}" ; + + ffmpeg_date="$( jq -r "${jq_arg}" -- .ffmpeg.releases.json )" + + var='FFMPEG_VERSION' ; + delim="$(mk_delim "${var}")" ; + open_ml_var "${delim}" "${var}" ; + jq_arg='.[]|select(.date == $date)|.versions[]|select(startswith("N-"))' ; + jq -r --arg date "${ffmpeg_date}" "${jq_arg}" -- .ffmpeg.releases.json ; + close_ml_var "${delim}" "${var}" ; + unset -v delim jq_arg var ; + } >> "${GITHUB_OUTPUT}" + cat -v "${GITHUB_OUTPUT}" + rm -v -f .ffmpeg.releases.json + + containerise: + needs: ['info'] + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log into GitHub Container Registry + run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true - tags: ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - cache-from: type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - cache-to: type=inline + tags: ghcr.io/${{ needs.info.outputs.lowercase-github-actor }}/${{ env.IMAGE_NAME }}:${{ needs.info.outputs.tag }} + cache-from: | + type=registry,ref=ghcr.io/${{ needs.info.outputs.lowercase-github-repository_owner }}/${{ env.IMAGE_NAME }}:latest + type=gha build-args: | IMAGE_NAME=${{ env.IMAGE_NAME }} + FFMPEG_DATE=${{ needs.info.outputs.ffmpeg-date }} + FFMPEG_VERSION=${{ needs.info.outputs.ffmpeg-version }} + YTDLP_DATE=${{ fromJSON(needs.info.outputs.ytdlp-latest-release).tag.name }} From e0e81c3a2c39742c6f8d88d92c43ff0ca36b53fa Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 04:59:55 -0400 Subject: [PATCH 003/103] Create get-tag/action.yml --- .github/actions/get-tag/action.yml | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/actions/get-tag/action.yml diff --git a/.github/actions/get-tag/action.yml b/.github/actions/get-tag/action.yml new file mode 100644 index 00000000..59ab43f3 --- /dev/null +++ b/.github/actions/get-tag/action.yml @@ -0,0 +1,41 @@ +name: Get tag +description: Get tag name from GITHUB_REF environment variable +inputs: + strip_v: + required: false + default: false + description: Whether to strip "v" from the tag or not +outputs: + tag: + value: ${{ steps.set.outputs.tag }} + description: Git tag name + +runs: + using: 'composite' + steps: + - name: Set outputs + id: 'set' + env: + INPUT_STRIP_V '${{ inputs.strip_v }}' + shell: 'bash' + run: | + tag="${GITHUB_REF}" + printf -- 'Manipulating string: %s\n' "${tag}" + test -n "${tag}" || exit 1 + + case "${tag}" in + (refs/tags/*) tag="${tag#refs/tags/}" ;; + (*) printf -- 'Not a tag ref\n' ; exit 2 ;; + esac + + if [ 'true' = "${INPUT_STRIP_V,,}" ] + then + tag="${tag#[Vv]}" + fi + + set_sl_var() { local f='%s=%s\n' ; printf -- "${f}" "$@" ; } ; + + set_sl_var tag "${tag}" >> "${GITHUB_OUTPUT}" + + set_sl_var 'tag ' " ${tag}" + From 4d179fbd427c0a559bd7cd634f346f50f16560c6 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 05:03:24 -0400 Subject: [PATCH 004/103] Use the local `get-tag` action --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dea6c23f..621c31a5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,10 +20,10 @@ jobs: ytdlp-latest-release: ${{ steps.yt-dlp.outputs.latest-release }} ytdlp-releases: ${{ steps.yt-dlp.outputs.releases }} steps: + - uses: actions/checkout@v4 - name: Get tag id: tag - uses: dawidd6/action-get-tag@v1 - - uses: actions/checkout@v4 + uses: ./.github/actions/get-tag - name: Lowercase github username id: github-actor uses: ./.github/actions/string-case From 7923d03387aca14ef0c50c29e464b00aff726297 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 05:09:15 -0400 Subject: [PATCH 005/103] fixup: missing colon --- .github/actions/get-tag/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/get-tag/action.yml b/.github/actions/get-tag/action.yml index 59ab43f3..7f554c70 100644 --- a/.github/actions/get-tag/action.yml +++ b/.github/actions/get-tag/action.yml @@ -16,7 +16,7 @@ runs: - name: Set outputs id: 'set' env: - INPUT_STRIP_V '${{ inputs.strip_v }}' + INPUT_STRIP_V: '${{ inputs.strip_v }}' shell: 'bash' run: | tag="${GITHUB_REF}" From 9de2728f35cc5dbc4a2a5e4e44d11194cd2bd04b Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 06:50:03 -0400 Subject: [PATCH 006/103] Create media__tasks.py + `wait_for_premiere` --- tubesync/sync/models/media__tasks.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tubesync/sync/models/media__tasks.py diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py new file mode 100644 index 00000000..35f8595f --- /dev/null +++ b/tubesync/sync/models/media__tasks.py @@ -0,0 +1,22 @@ +from django.utils import timezone + + +def wait_for_premiere(self): + hours = lambda td: 1+int((24*td.days)+(td.seconds/(60*60))) + + in_hours = None + if self.has_metadata or not self.published: + return (False, in_hours,) + + now = timezone.now() + if self.published < now: + in_hours = 0 + self.manual_skip = False + self.skip = False + else: + in_hours = hours(self.published - now) + self.manual_skip = True + self.title = _(f'Premieres in {in_hours} hours') + + return (True, in_hours,) + From 9ee28befe0ffcfe0ad52cd04f0232d820fc2c7d1 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 06:54:42 -0400 Subject: [PATCH 007/103] Add `wait_for_premiere` function --- tubesync/sync/models/media.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tubesync/sync/models/media.py b/tubesync/sync/models/media.py index daaf723d..3529d889 100644 --- a/tubesync/sync/models/media.py +++ b/tubesync/sync/models/media.py @@ -40,6 +40,7 @@ from ._migrations import ( media_file_storage, get_media_thumb_path, get_media_file_path, ) from ._private import _srctype_dict, _nfo_element +from .media__tasks import wait_for_premiere from .source import Source @@ -1219,3 +1220,7 @@ class Media(models.Model): except OSError as e: pass + +# add imported functions +Media.wait_for_premiere = wait_for_premiere + From 31fc6adc1190a98818ca880dbf58d0033b75c7fd Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 07:07:22 -0400 Subject: [PATCH 008/103] Use `Media.wait_for_premiere` function --- tubesync/sync/tasks.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index d29e8239..84eaad69 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -887,26 +887,19 @@ def rename_all_media_for_source(source_id): @background(schedule=dict(priority=0, run_at=60), queue=Val(TaskQueue.DB), remove_existing_tasks=True) def wait_for_media_premiere(media_id): - hours = lambda td: 1+int((24*td.days)+(td.seconds/(60*60))) - try: media = Media.objects.get(pk=media_id) except Media.DoesNotExist as e: raise InvalidTaskError(_('no such media')) from e else: - if media.has_metadata: + valid, hours = media.wait_for_premiere() + if not valid: return - now = timezone.now() - if media.published < now: - media.manual_skip = False - media.skip = False - # the download tasks start after the media is saved - else: - media.manual_skip = True - media.title = _(f'Premieres in {hours(media.published - now)} hours') + + if hours: task = get_media_premiere_task(media_id) if task: - update_task_status(task, f'available in {hours(media.published - now)} hours') + update_task_status(task, f'available in {hours} hours') save_model(media) From 7390a2b2b18c7d7ad23d9292d66145f21c7ca8a6 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 07:41:37 -0400 Subject: [PATCH 009/103] Add `download_checklist` function --- tubesync/sync/models/media__tasks.py | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index 35f8595f..106a27c6 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -1,6 +1,52 @@ +from common.logger import log +from common.errors import ( + NoMetadataException, +) from django.utils import timezone +def download_checklist(self, skip_checks=False): + media = self + if skip_checks: + return True + + if not media.source.download_media: + log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' + f'the source {media.source} has since been marked to not download, ' + f'not downloading') + return False + if media.skip or media.manual_skip: + # Media was toggled to be skipped after the task was scheduled + log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' + f'it is now marked to be skipped, not downloading') + return False + # metadata is required to generate the proper filepath + if not media.has_metadata: + raise NoMetadataException('Metadata is not yet available.') + downloaded_file_exists = ( + media.downloaded and + media.has_metadata and + ( + media.media_file_exists or + media.filepath.exists() + ) + ) + if downloaded_file_exists: + # Media has been marked as downloaded before the download_media task was fired, + # skip it + log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' + f'it has already been marked as downloaded, not downloading again') + return False + max_cap_age = media.source.download_cap_date + published = media.published + if max_cap_age and published: + if published <= max_cap_age: + log.warn(f'Download task triggered media: {media} (UUID: {media.pk}) but ' + f'the source has a download cap and the media is now too old, ' + f'not downloading') + return False + + def wait_for_premiere(self): hours = lambda td: 1+int((24*td.days)+(td.seconds/(60*60))) From fb4185a9b14bbdf7946aaa9b10c3f98f77a8231e Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 07:44:21 -0400 Subject: [PATCH 010/103] Add `Media.download_checklist` function --- tubesync/sync/models/media.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/models/media.py b/tubesync/sync/models/media.py index 3529d889..9b033338 100644 --- a/tubesync/sync/models/media.py +++ b/tubesync/sync/models/media.py @@ -40,7 +40,9 @@ from ._migrations import ( media_file_storage, get_media_thumb_path, get_media_file_path, ) from ._private import _srctype_dict, _nfo_element -from .media__tasks import wait_for_premiere +from .media__tasks import ( + download_checklist, wait_for_premiere, +) from .source import Source @@ -1222,5 +1224,6 @@ class Media(models.Model): # add imported functions +Media.download_checklist = download_checklist Media.wait_for_premiere = wait_for_premiere From e507a63362ed72f775c6c88654b5ee20e15192ad Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 07:50:54 -0400 Subject: [PATCH 011/103] Use `Media.download_checklist` function --- tubesync/sync/tasks.py | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 84eaad69..ea0cb6a1 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -604,7 +604,7 @@ def download_media_thumbnail(media_id, url): @background(schedule=dict(priority=30, run_at=60), queue=Val(TaskQueue.NET), remove_existing_tasks=True) -def download_media(media_id): +def download_media(media_id, override=False): ''' Downloads the media to disk and attaches it to the Media instance. ''' @@ -613,41 +613,10 @@ def download_media(media_id): except Media.DoesNotExist as e: # Task triggered but the media no longer exists, do nothing raise InvalidTaskError(_('no such media')) from e - if not media.source.download_media: - log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' - f'the source {media.source} has since been marked to not download, ' - f'not downloading') - return - if media.skip or media.manual_skip: - # Media was toggled to be skipped after the task was scheduled - log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' - f'it is now marked to be skipped, not downloading') - return - # metadata is required to generate the proper filepath - if not media.has_metadata: - raise NoMetadataException('Metadata is not yet available.') - downloaded_file_exists = ( - media.downloaded and - media.has_metadata and - ( - media.media_file_exists or - media.filepath.exists() - ) - ) - if downloaded_file_exists: - # Media has been marked as downloaded before the download_media task was fired, - # skip it - log.warn(f'Download task triggered for media: {media} (UUID: {media.pk}) but ' - f'it has already been marked as downloaded, not downloading again') - return - max_cap_age = media.source.download_cap_date - published = media.published - if max_cap_age and published: - if published <= max_cap_age: - log.warn(f'Download task triggered media: {media} (UUID: {media.pk}) but ' - f'the source has a download cap and the media is now too old, ' - f'not downloading') + else: + if not media.download_checklist(override): return + filepath = media.filepath container = format_str = None log.info(f'Downloading media: {media} (UUID: {media.pk}) to: "{filepath}"') From 54b7e261f1f1f3a325015c1a8d72e2fe2e69cc69 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 08:16:18 -0400 Subject: [PATCH 012/103] Add `download_finished` function --- tubesync/sync/models/media__tasks.py | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index 106a27c6..fef043e3 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -1,3 +1,4 @@ +import os from common.logger import log from common.errors import ( NoMetadataException, @@ -47,6 +48,50 @@ def download_checklist(self, skip_checks=False): return False +def download_finished(self, format_str, container, downloaded_filepath=None): + media = self + if downloaded_filepath is None: + downloaded_filepath = self.filepath + filepath = Path(downloaded_filepath) + # Media has been downloaded successfully + log.info(f'Successfully downloaded media: {media} (UUID: {media.pk}) to: ' + f'"{filepath}"') + # Link the media file to the object and update info about the download + media.media_file.name = str(media.source.type_directory_path / media.filename) + media.downloaded = True + media.download_date = timezone.now() + media.downloaded_filesize = os.path.getsize(filepath) + media.downloaded_container = container + if '+' in format_str: + # Seperate audio and video streams + vformat_code, aformat_code = format_str.split('+') + aformat = media.get_format_by_code(aformat_code) + vformat = media.get_format_by_code(vformat_code) + media.downloaded_format = vformat['format'] + media.downloaded_height = vformat['height'] + media.downloaded_width = vformat['width'] + media.downloaded_audio_codec = aformat['acodec'] + media.downloaded_video_codec = vformat['vcodec'] + media.downloaded_container = container + media.downloaded_fps = vformat['fps'] + media.downloaded_hdr = vformat['is_hdr'] + else: + # Combined stream or audio-only stream + cformat_code = format_str + cformat = media.get_format_by_code(cformat_code) + media.downloaded_audio_codec = cformat['acodec'] + if cformat['vcodec']: + # Combined + media.downloaded_format = cformat['format'] + media.downloaded_height = cformat['height'] + media.downloaded_width = cformat['width'] + media.downloaded_video_codec = cformat['vcodec'] + media.downloaded_fps = cformat['fps'] + media.downloaded_hdr = cformat['is_hdr'] + else: + media.downloaded_format = 'audio' + + def wait_for_premiere(self): hours = lambda td: 1+int((24*td.days)+(td.seconds/(60*60))) From f77a6573229466430f6a02e47ec1fa7933159a0d Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 08:18:21 -0400 Subject: [PATCH 013/103] Add `Media.download_finished` function --- tubesync/sync/models/media.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/models/media.py b/tubesync/sync/models/media.py index 9b033338..6eb0ed76 100644 --- a/tubesync/sync/models/media.py +++ b/tubesync/sync/models/media.py @@ -41,7 +41,7 @@ from ._migrations import ( ) from ._private import _srctype_dict, _nfo_element from .media__tasks import ( - download_checklist, wait_for_premiere, + download_checklist, download_finished, wait_for_premiere, ) from .source import Source @@ -1225,5 +1225,6 @@ class Media(models.Model): # add imported functions Media.download_checklist = download_checklist +Media.download_finished = download_finished Media.wait_for_premiere = wait_for_premiere From 151640ae459c5c2d159e185a6e71298d39bd9ff9 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 08:25:15 -0400 Subject: [PATCH 014/103] Use `Media.download_finished` function --- tubesync/sync/tasks.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index ea0cb6a1..0467a4fd 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -649,42 +649,7 @@ def download_media(media_id, override=False): raise DownloadFailedException(err) # Media has been downloaded successfully - log.info(f'Successfully downloaded media: {media} (UUID: {media.pk}) to: ' - f'"{filepath}"') - # Link the media file to the object and update info about the download - media.media_file.name = str(media.source.type_directory_path / media.filename) - media.downloaded = True - media.download_date = timezone.now() - media.downloaded_filesize = os.path.getsize(filepath) - media.downloaded_container = container - if '+' in format_str: - # Seperate audio and video streams - vformat_code, aformat_code = format_str.split('+') - aformat = media.get_format_by_code(aformat_code) - vformat = media.get_format_by_code(vformat_code) - media.downloaded_format = vformat['format'] - media.downloaded_height = vformat['height'] - media.downloaded_width = vformat['width'] - media.downloaded_audio_codec = aformat['acodec'] - media.downloaded_video_codec = vformat['vcodec'] - media.downloaded_container = container - media.downloaded_fps = vformat['fps'] - media.downloaded_hdr = vformat['is_hdr'] - else: - # Combined stream or audio-only stream - cformat_code = format_str - cformat = media.get_format_by_code(cformat_code) - media.downloaded_audio_codec = cformat['acodec'] - if cformat['vcodec']: - # Combined - media.downloaded_format = cformat['format'] - media.downloaded_height = cformat['height'] - media.downloaded_width = cformat['width'] - media.downloaded_video_codec = cformat['vcodec'] - media.downloaded_fps = cformat['fps'] - media.downloaded_hdr = cformat['is_hdr'] - else: - media.downloaded_format = 'audio' + media.download_finished(format_str, container, filepath) save_model(media) # If selected, copy the thumbnail over as well if media.source.copy_thumbnails: From 346d66ff6921450a1b93735300e8da2db6ec869b Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 08:27:02 -0400 Subject: [PATCH 015/103] Add a blank line --- tubesync/sync/models/media__tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index fef043e3..bf27813e 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -53,6 +53,7 @@ def download_finished(self, format_str, container, downloaded_filepath=None): if downloaded_filepath is None: downloaded_filepath = self.filepath filepath = Path(downloaded_filepath) + # Media has been downloaded successfully log.info(f'Successfully downloaded media: {media} (UUID: {media.pk}) to: ' f'"{filepath}"') From f0a4c9dea4db9bbe1ef458f0908d8e570e5ac8d5 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 08:49:04 -0400 Subject: [PATCH 016/103] Use storage location for `media_file` --- tubesync/sync/models/media__tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index bf27813e..d63f7b31 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -4,6 +4,7 @@ from common.errors import ( NoMetadataException, ) from django.utils import timezone +from ..choices import Val, SourceResolution def download_checklist(self, skip_checks=False): @@ -58,7 +59,7 @@ def download_finished(self, format_str, container, downloaded_filepath=None): log.info(f'Successfully downloaded media: {media} (UUID: {media.pk}) to: ' f'"{filepath}"') # Link the media file to the object and update info about the download - media.media_file.name = str(media.source.type_directory_path / media.filename) + self.media_file.name = str(filepath.relative_to(self.media_file.storage.location)) media.downloaded = True media.download_date = timezone.now() media.downloaded_filesize = os.path.getsize(filepath) @@ -90,7 +91,7 @@ def download_finished(self, format_str, container, downloaded_filepath=None): media.downloaded_fps = cformat['fps'] media.downloaded_hdr = cformat['is_hdr'] else: - media.downloaded_format = 'audio' + self.downloaded_format = Val(SourceResolution.AUDIO) def wait_for_premiere(self): From a68c82d975c083aee75e443fffefe805b3afabf7 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 18:37:11 -0400 Subject: [PATCH 017/103] Add `bgutil-ytdlp-pot-provider` plugin --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index aed25ce3..5d76b308 100644 --- a/Pipfile +++ b/Pipfile @@ -25,3 +25,4 @@ emoji = "*" brotli = "*" html5lib = "*" yt-dlp-get-pot = "*" +bgutil-ytdlp-pot-provider = "*" From 12aa6b413470a71bbf4c637ba6d1f581783efeaf Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 19:55:56 -0400 Subject: [PATCH 018/103] Use token_server.conf --- config/root/etc/nginx/nginx.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/root/etc/nginx/nginx.conf b/config/root/etc/nginx/nginx.conf index e6b99b68..e4e9140e 100644 --- a/config/root/etc/nginx/nginx.conf +++ b/config/root/etc/nginx/nginx.conf @@ -135,4 +135,7 @@ http { } } + # Proof-of-Origin Token Server + include /etc/nginx/token_server.conf; + } From 4abd768ba8c56e93ea3c15aa9c898b1a42f047fb Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 19:59:09 -0400 Subject: [PATCH 019/103] Create token_server.conf --- config/root/etc/nginx/token_server.conf | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/root/etc/nginx/token_server.conf diff --git a/config/root/etc/nginx/token_server.conf b/config/root/etc/nginx/token_server.conf new file mode 100644 index 00000000..fbfd5979 --- /dev/null +++ b/config/root/etc/nginx/token_server.conf @@ -0,0 +1,15 @@ +upstream token_server { + server 127.0.0.2:4416 down; +} + +server { + + # Ports + listen 4416; + listen [::]:4416; + + location / { + proxy_pass http://token_server; + proxy_redirect default; + } +} From 8819a945966acedb6b510f4db0bd41e859dad5d9 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 21:32:54 -0400 Subject: [PATCH 020/103] Add `server_name` --- config/root/etc/nginx/token_server.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/root/etc/nginx/token_server.conf b/config/root/etc/nginx/token_server.conf index fbfd5979..a49f3a3e 100644 --- a/config/root/etc/nginx/token_server.conf +++ b/config/root/etc/nginx/token_server.conf @@ -8,6 +8,9 @@ server { listen 4416; listen [::]:4416; + # Server domain name + server_name _; + location / { proxy_pass http://token_server; proxy_redirect default; From 5f797d091541f5251d1e374b474f76f707a98759 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 21:57:37 -0400 Subject: [PATCH 021/103] Add `YT_POT_BGUTIL_BASE_URL` environment variable --- config/root/etc/nginx/nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/root/etc/nginx/nginx.conf b/config/root/etc/nginx/nginx.conf index e4e9140e..1b544f44 100644 --- a/config/root/etc/nginx/nginx.conf +++ b/config/root/etc/nginx/nginx.conf @@ -5,6 +5,8 @@ worker_processes auto; worker_cpu_affinity auto; pid /run/nginx.pid; +env YT_POT_BGUTIL_BASE_URL; + events { worker_connections 1024; } From 0919b03143ea379473c35f47045c1b6db5a05919 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 22:11:42 -0400 Subject: [PATCH 022/103] Set the scheme from `YT_POT_BGUTIL_BASE_URL` --- config/root/etc/nginx/token_server.conf | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/root/etc/nginx/token_server.conf b/config/root/etc/nginx/token_server.conf index a49f3a3e..057cade5 100644 --- a/config/root/etc/nginx/token_server.conf +++ b/config/root/etc/nginx/token_server.conf @@ -11,8 +11,18 @@ server { # Server domain name server_name _; + set_by_lua_block $pot_url_scheme { + local haystack = os.getenv('YT_POT_BGUTIL_BASE_URL') or '' + local needle = 'https://' + local scheme = 'http' + if haystack:sub(1, #needle) == needle then + scheme = 'https' + end + return scheme + } + location / { - proxy_pass http://token_server; + proxy_pass $pot_url_scheme://token_server; proxy_redirect default; } } From f7d3367a9ff284288a038228794706de3a657bc6 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 23:37:23 -0400 Subject: [PATCH 023/103] Use the environment variable directly --- config/root/etc/nginx/token_server.conf | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/config/root/etc/nginx/token_server.conf b/config/root/etc/nginx/token_server.conf index 057cade5..6c0c4e3a 100644 --- a/config/root/etc/nginx/token_server.conf +++ b/config/root/etc/nginx/token_server.conf @@ -11,18 +11,19 @@ server { # Server domain name server_name _; - set_by_lua_block $pot_url_scheme { - local haystack = os.getenv('YT_POT_BGUTIL_BASE_URL') or '' - local needle = 'https://' - local scheme = 'http' - if haystack:sub(1, #needle) == needle then - scheme = 'https' + set_by_lua_block $pot_url { + local default = 'http://token_server' + local url = os.getenv('YT_POT_BGUTIL_BASE_URL') + if not url then + return default end - return scheme + if #url and url:find('://') then + return url + end + return default } location / { - proxy_pass $pot_url_scheme://token_server; - proxy_redirect default; + proxy_pass $pot_url; } } From 14877a8c7d1c2b9a7113b1af4717346010e62e51 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 16 May 2025 23:59:13 -0400 Subject: [PATCH 024/103] Set `YT_POT_BGUTIL_BASE_URL` for the user --- config/root/etc/s6-overlay/s6-rc.d/nginx/run | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/root/etc/s6-overlay/s6-rc.d/nginx/run b/config/root/etc/s6-overlay/s6-rc.d/nginx/run index 63653343..de89b819 100755 --- a/config/root/etc/s6-overlay/s6-rc.d/nginx/run +++ b/config/root/etc/s6-overlay/s6-rc.d/nginx/run @@ -2,4 +2,14 @@ cd / +https="${TUBESYNC_POT_HTTPS:+https}" +ip_address="${TUBESYNC_POT_IPADDR}" +port="${TUBESYNC_POT_PORT:+:}${TUBESYNC_POT_PORT}" + +if [ -n "${ip_address}" ] +then + YT_POT_BGUTIL_BASE_URL="${https:-http}://${ip_address}${port}" + export YT_POT_BGUTIL_BASE_URL +fi + exec /usr/bin/openresty -c /etc/nginx/nginx.conf -e stderr From 98f3bba9e8306631d18280172a4d8261b8491219 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 00:31:55 -0400 Subject: [PATCH 025/103] Support `--link` variables --- config/root/etc/s6-overlay/s6-rc.d/nginx/run | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/root/etc/s6-overlay/s6-rc.d/nginx/run b/config/root/etc/s6-overlay/s6-rc.d/nginx/run index de89b819..fff967d1 100755 --- a/config/root/etc/s6-overlay/s6-rc.d/nginx/run +++ b/config/root/etc/s6-overlay/s6-rc.d/nginx/run @@ -3,7 +3,8 @@ cd / https="${TUBESYNC_POT_HTTPS:+https}" -ip_address="${TUBESYNC_POT_IPADDR}" +ip_address="${TUBESYNC_POT_IPADDR:-${POTSERVER_PORT_4416_TCP_ADDR}}" +: "${TUBESYNC_POT_PORT:=${POTSERVER_PORT_4416_TCP_PORT}}" port="${TUBESYNC_POT_PORT:+:}${TUBESYNC_POT_PORT}" if [ -n "${ip_address}" ] From ea911e4bf91645e987f501c6f9d7def3930fe2a0 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 01:49:32 -0400 Subject: [PATCH 026/103] Bump version --- tubesync/tubesync/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/tubesync/settings.py b/tubesync/tubesync/settings.py index 7f5922ae..798bd252 100644 --- a/tubesync/tubesync/settings.py +++ b/tubesync/tubesync/settings.py @@ -8,7 +8,7 @@ CONFIG_BASE_DIR = BASE_DIR DOWNLOADS_BASE_DIR = BASE_DIR -VERSION = '0.15.2' +VERSION = '0.15.3' SECRET_KEY = '' DEBUG = False ALLOWED_HOSTS = [] From b93b4e1d47a49d94f2d71a5eb8402e64a9d7461b Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 08:22:56 -0400 Subject: [PATCH 027/103] Add a step for `ruff check` --- .github/workflows/ci.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b7de46be..332801e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,7 +94,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pipenv + pip install pipenv uv PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - name: Set up Django environment run: | @@ -102,6 +102,12 @@ jobs: cp -v -a -t "${Python3_ROOT_DIR}"/lib/python3.*/site-packages/background_task/ patches/background_task/* cp -v -a -t "${Python3_ROOT_DIR}"/lib/python3.*/site-packages/yt_dlp/ patches/yt_dlp/* cd tubesync && python3 -B manage.py collectstatic --no-input --link + - name: Check with ruff + continue-on-error: true + run: | + cd tubesync && uvx ruff check --output-format github \ + --target-version py310 --isolated \ + --ignore E701,E722,E731 - name: Run Django tests run: cd tubesync && python3 -B -W default manage.py test --verbosity=2 From c22ce9b67c053719af7e3c92fefc6e20832d1c2a Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 08:42:06 -0400 Subject: [PATCH 028/103] Fixes for `ruff check` in youtube.py --- tubesync/sync/youtube.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index 9814db24..022fbcf9 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -6,7 +6,6 @@ import os -from collections import namedtuple from common.logger import log from copy import deepcopy from pathlib import Path @@ -102,7 +101,7 @@ def get_channel_image_info(url): avatar_url = thumbnail['url'] if thumbnail['id'] == 'banner_uncropped': banner_url = thumbnail['url'] - if banner_url != None and avatar_url != None: + if banner_url is not None and avatar_url is not None: break return avatar_url, banner_url @@ -143,7 +142,7 @@ def get_media_info(url, /, *, days=None, info_json=None): if days is not None: try: days = int(str(days), 10) - except Exception as e: + except (TypeError, ValueError): days = None start = ( f'yesterday-{days!s}days' if days else None From 2785c7d3878fee1db60c5b432f432bf735094316 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 09:06:26 -0400 Subject: [PATCH 029/103] Fixes for `ruff check` in views.py --- tubesync/sync/views.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 10cfc5db..2fd4dec8 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -35,7 +35,6 @@ from .tasks import (map_task_to_instance, get_error_message, from .choices import (Val, MediaServerType, SourceResolution, IndexSchedule, YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) -from . import signals from . import youtube @@ -258,7 +257,7 @@ class ValidateSourceView(FormView): self.key = youtube.get_channel_id( index_url.replace('/channel/', '/') ) - except youtube.YouTubeError as e: + except youtube.YouTubeError: # It did not work, revert to previous behavior self.key = old_key self.source_type = old_source_type @@ -296,10 +295,13 @@ class EditSourceMixin: def form_valid(self, form: Form): # Perform extra validation to make sure the media_format is valid obj = form.save(commit=False) - source_type = form.cleaned_data['media_format'] + # temporarily use media_format from the form + saved_media_format = obj.media_format + obj.media_format = form.cleaned_data['media_format'] example_media_file = obj.get_example_media_format() + obj.media_format = saved_media_format - if example_media_file == '': + if '' == example_media_file: form.add_error( 'media_format', ValidationError(self.errors['invalid_media_format']) @@ -307,12 +309,16 @@ class EditSourceMixin: # Check for suspicious file path(s) try: - targetCheck = form.cleaned_data['directory']+"/.virt" - newdir = safe_join(settings.DOWNLOAD_ROOT,targetCheck) + targetCheck = form.cleaned_data['directory'] + '/.virt' + safe_join(settings.DOWNLOAD_ROOT, targetCheck) except SuspiciousFileOperation: form.add_error( 'directory', - ValidationError(self.errors['dir_outside_dlroot'].replace("%BASEDIR%",str(settings.DOWNLOAD_ROOT))) + ValidationError( + self.errors['dir_outside_dlroot'].replace( + "%BASEDIR%", str(settings.DOWNLOAD_ROOT) + ) + ), ) if form.errors: From d956a99110b63f6e139914bb71f10dd815f21a94 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 09:14:24 -0400 Subject: [PATCH 030/103] Fixes for `ruff check` in tasks.py --- tubesync/sync/tasks.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 0467a4fd..1f94e231 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -6,7 +6,6 @@ import os import json -import math import random import requests import time @@ -14,9 +13,8 @@ import uuid from io import BytesIO from hashlib import sha1 from pathlib import Path -from datetime import datetime, timedelta +from datetime import timedelta from shutil import copyfile, rmtree -from PIL import Image from django import db from django.conf import settings from django.core.files.base import ContentFile @@ -36,7 +34,7 @@ from common.utils import ( django_queryset_generator as qs_gen, remove_enclosed, ) from .choices import Val, TaskQueue from .models import Source, Media, MediaServer -from .utils import ( get_remote_image, resize_image_to_height, delete_file, +from .utils import ( get_remote_image, resize_image_to_height, write_text_file, filter_response, ) from .youtube import YouTubeError @@ -226,7 +224,7 @@ def save_model(instance): @atomic(durable=False) def schedule_media_servers_update(): # Schedule a task to update media servers - log.info(f'Scheduling media server updates') + log.info('Scheduling media server updates') verbose_name = _('Request media server rescan for "{}"') for mediaserver in MediaServer.objects.all(): rescan_media_server( @@ -416,7 +414,7 @@ def download_source_images(source_id): log.info(f'Thumbnail URL for source with ID: {source_id} / {source} ' f'Avatar: {avatar} ' f'Banner: {banner}') - if banner != None: + if banner is not None: url = banner i = get_remote_image(url) image_file = BytesIO() @@ -432,7 +430,7 @@ def download_source_images(source_id): f.write(django_file.read()) i = image_file = None - if avatar != None: + if avatar is not None: url = avatar i = get_remote_image(url) image_file = BytesIO() @@ -845,7 +843,7 @@ def delete_all_media_for_source(source_id, source_name, source_directory): assert source_directory try: source = Source.objects.get(pk=source_id) - except Source.DoesNotExist as e: + except Source.DoesNotExist: # Task triggered but the source no longer exists, do nothing log.warn(f'Task delete_all_media_for_source(pk={source_id}) called but no ' f'source exists with ID: {source_id}') From 34dfb52bf08b40d5d5768a0af3233a8de5b24c7b Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 09:43:17 -0400 Subject: [PATCH 031/103] Fixes for `ruff check` in signals.py --- tubesync/sync/signals.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index 790ce1c2..69254146 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -10,11 +10,11 @@ from django.utils.translation import gettext_lazy as _ from background_task.signals import task_failed from background_task.models import Task from common.logger import log -from .models import Source, Media, MediaServer, Metadata +from .models import Source, Media, Metadata from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task, download_media_thumbnail, download_media_metadata, map_task_to_instance, check_source_directory_exists, - download_media, rescan_media_server, download_source_images, + download_media, download_source_images, delete_all_media_for_source, save_all_media_for_source, rename_media, get_media_metadata_task, get_media_download_task) from .utils import delete_file, glob_quote, mkdir_p @@ -270,7 +270,7 @@ def media_post_save(sender, instance, created, **kwargs): if not (media_file_exists or existing_media_download_task): # The file was deleted after it was downloaded, skip this media. if instance.can_download and instance.downloaded: - skip_changed = True != instance.skip + skip_changed = True if not instance.skip else False instance.skip = True downloaded = False if (instance.source.download_media and instance.can_download) and not ( @@ -374,13 +374,13 @@ def media_post_delete(sender, instance, **kwargs): try: p.rmdir() log.info(f'Deleted directory for: {instance} path: {p!s}') - except OSError as e: + except OSError: pass # Delete the directory itself try: other_path.rmdir() log.info(f'Deleted directory for: {instance} path: {other_path!s}') - except OSError as e: + except OSError: pass # Get all files that start with the bare file path all_related_files = video_path.parent.glob(f'{glob_quote(video_path.with_suffix("").name)}*') From 565fe79183dee26d855c57b9401e29effa6350ac Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 09:47:57 -0400 Subject: [PATCH 032/103] Fixes for `ruff check` in models/source.py --- tubesync/sync/models/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 74f75278..b7e2b3da 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -510,7 +510,7 @@ class Source(db.models.Model): def get_example_media_format(self): try: return self.media_format.format(**self.example_media_format_dict) - except Exception as e: + except Exception: return '' def is_regex_match(self, media_item_title): From 6f8bc52c0a8e910fa4b1f187e29ffef7db39d1cb Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 09:53:30 -0400 Subject: [PATCH 033/103] Fixes for `ruff check` in models/media.py --- tubesync/sync/models/media.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/models/media.py b/tubesync/sync/models/media.py index 6eb0ed76..4af39c7a 100644 --- a/tubesync/sync/models/media.py +++ b/tubesync/sync/models/media.py @@ -17,7 +17,6 @@ from common.logger import log from common.errors import NoFormatException from common.utils import ( clean_filename, clean_emoji, - django_queryset_generator as qs_gen, ) from ..youtube import ( get_media_info as get_youtube_media_info, @@ -688,7 +687,7 @@ class Media(models.Model): pass setattr(self, '_cached_metadata_dict', data) return data - except Exception as e: + except Exception: return {} @@ -1219,7 +1218,7 @@ class Media(models.Model): parent_dir.rmdir() log.info(f'Removed empty directory: {parent_dir!s}') parent_dir = parent_dir.parent - except OSError as e: + except OSError: pass From 4ead042235bb73138297df208daa0e60834a3b30 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:10:37 -0400 Subject: [PATCH 034/103] Fixes for `ruff check` in models/__init__.py --- tubesync/sync/models/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tubesync/sync/models/__init__.py b/tubesync/sync/models/__init__.py index d7ed077c..a850a4e0 100644 --- a/tubesync/sync/models/__init__.py +++ b/tubesync/sync/models/__init__.py @@ -17,3 +17,9 @@ from .media import Media from .metadata import Metadata from .metadata_format import MetadataFormat +__all__ = [ + 'get_media_file_path', 'get_media_thumb_path', + 'media_file_storage', 'MediaServer', 'Source', + 'Media', 'Metadata', 'MetadataFormat', +] + From 48c9fb1ca32edda29f180fb9ddbc928792958b40 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:15:30 -0400 Subject: [PATCH 035/103] Fixes for `ruff check` in mediaservers.py --- tubesync/sync/mediaservers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tubesync/sync/mediaservers.py b/tubesync/sync/mediaservers.py index ceab239f..e141238d 100644 --- a/tubesync/sync/mediaservers.py +++ b/tubesync/sync/mediaservers.py @@ -117,9 +117,6 @@ class PlexMediaServer(MediaServer): raise ValidationError('Plex Media Server "port" must be between 1 ' 'and 65535') options = self.object.options - if 'token' not in options: - raise ValidationError('Plex Media Server requires a "token"') - token = options['token'].strip() if 'token' not in options: raise ValidationError('Plex Media Server requires a "token"') if 'libraries' not in options: From c07ed8eb040ec9ef7064f8b0fa015f40d3546f62 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:18:04 -0400 Subject: [PATCH 036/103] Fixes for `ruff check` in hooks.py --- tubesync/sync/hooks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tubesync/sync/hooks.py b/tubesync/sync/hooks.py index 3bb3ce0d..467e2df1 100644 --- a/tubesync/sync/hooks.py +++ b/tubesync/sync/hooks.py @@ -1,9 +1,7 @@ import os -import yt_dlp from common.logger import log from common.utils import remove_enclosed -from django.conf import settings progress_hook = { From 60579ab883d3357e66b2e4db76fbca7103e7f663 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:19:48 -0400 Subject: [PATCH 037/103] Fixes for `ruff check` in filtering.py --- tubesync/sync/filtering.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tubesync/sync/filtering.py b/tubesync/sync/filtering.py index 3f7023c1..ef206696 100644 --- a/tubesync/sync/filtering.py +++ b/tubesync/sync/filtering.py @@ -5,7 +5,6 @@ from common.logger import log from .models import Media from datetime import datetime -from django.utils import timezone from .overrides.custom_filter import filter_custom From 087e2f2e5d39565e53dccf11b0726613ee194f3e Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:22:16 -0400 Subject: [PATCH 038/103] Fixes for `ruff check` in fields.py --- tubesync/sync/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/fields.py b/tubesync/sync/fields.py index 452fee77..648afc63 100644 --- a/tubesync/sync/fields.py +++ b/tubesync/sync/fields.py @@ -63,7 +63,7 @@ class CommaSepChoiceField(models.CharField): def __init__(self, *args, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, **kwargs): kwargs.setdefault('max_length', 128) self.separator = str(separator) - self.possible_choices = possible_choices or choices + self.possible_choices = possible_choices or kwargs['choices'] self.selected_choices = list() self.allow_all = allow_all self.all_label = all_label From 35d2aa7b2d87330c266fb6f7b91386318eb9fff4 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:23:48 -0400 Subject: [PATCH 039/103] Fixes for `ruff check` in urls.py --- tubesync/common/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tubesync/common/urls.py b/tubesync/common/urls.py index 1f29056d..a3e00a84 100644 --- a/tubesync/common/urls.py +++ b/tubesync/common/urls.py @@ -1,7 +1,6 @@ from django.conf import settings from django.urls import path from django.views.generic.base import RedirectView -from django.views.generic import TemplateView from django.http import HttpResponse from .views import error403, error404, error500, HealthCheckView From 58ed18519afc0d3136cf19d4ff7bf5c7ab941421 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 10:33:42 -0400 Subject: [PATCH 040/103] Fixes for `ruff check` in utils.py --- tubesync/common/utils.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tubesync/common/utils.py b/tubesync/common/utils.py index c4798943..5fc515df 100644 --- a/tubesync/common/utils.py +++ b/tubesync/common/utils.py @@ -84,14 +84,11 @@ def parse_database_connection_string(database_connection_string): f'invalid driver, must be one of {valid_drivers}') django_driver = django_backends.get(driver) host_parts = user_pass_host_port.split('@') - if len(host_parts) != 2: - raise DatabaseConnectionError(f'Database connection string netloc must be in ' - f'the format of user:pass@host') + user_pass_parts = host_parts[0].split(':') + if len(host_parts) != 2 or len(user_pass_parts) != 2: + raise DatabaseConnectionError('Database connection string netloc must be in ' + 'the format of user:pass@host') user_pass, host_port = host_parts - user_pass_parts = user_pass.split(':') - if len(user_pass_parts) != 2: - raise DatabaseConnectionError(f'Database connection string netloc must be in ' - f'the format of user:pass@host') username, password = user_pass_parts host_port_parts = host_port.split(':') if len(host_port_parts) == 1: @@ -113,13 +110,13 @@ def parse_database_connection_string(database_connection_string): f'65535, got {port}') else: # Malformed - raise DatabaseConnectionError(f'Database connection host must be a hostname or ' - f'a hostname:port combination') + raise DatabaseConnectionError('Database connection host must be a hostname or ' + 'a hostname:port combination') if database.startswith('/'): database = database[1:] if not database: - raise DatabaseConnectionError(f'Database connection string path must be a ' - f'string in the format of /databasename') + raise DatabaseConnectionError('Database connection string path must be a ' + 'string in the format of /databasename') if '/' in database: raise DatabaseConnectionError(f'Database connection string path can only ' f'contain a single string name, got: {database}') From 2e6f07652a510e5351d91d86b0cad6de7651cdba Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 19:57:09 -0400 Subject: [PATCH 041/103] Import signals We must import this file to have Django signals used. This should probably be done as part of app configuration instead of in views. --- tubesync/sync/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 2fd4dec8..4f683834 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -35,6 +35,7 @@ from .tasks import (map_task_to_instance, get_error_message, from .choices import (Val, MediaServerType, SourceResolution, IndexSchedule, YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) +from . import signals # noqa from . import youtube From 9e13870e6f3e63c54fc1bc048702e63a59020ef5 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 20:01:51 -0400 Subject: [PATCH 042/103] Use the `dict.get` function instead --- tubesync/sync/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/fields.py b/tubesync/sync/fields.py index 648afc63..e489ff08 100644 --- a/tubesync/sync/fields.py +++ b/tubesync/sync/fields.py @@ -63,7 +63,7 @@ class CommaSepChoiceField(models.CharField): def __init__(self, *args, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, **kwargs): kwargs.setdefault('max_length', 128) self.separator = str(separator) - self.possible_choices = possible_choices or kwargs['choices'] + self.possible_choices = possible_choices or kwargs.get('choices') self.selected_choices = list() self.allow_all = allow_all self.all_label = all_label From c85dd553dc7b8a79d38ba0583add2eb844717c66 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 20:13:44 -0400 Subject: [PATCH 043/103] Remove unused `NoMetadataException` --- tubesync/sync/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 1f94e231..483f6c14 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -28,7 +28,7 @@ from background_task.exceptions import InvalidTaskError from background_task.models import Task, CompletedTask from common.logger import log from common.errors import ( NoFormatException, NoMediaException, - NoMetadataException, NoThumbnailException, + NoThumbnailException, DownloadFailedException, ) from common.utils import ( django_queryset_generator as qs_gen, remove_enclosed, ) From 0fa5c9ef97db27ed55e6b660b1741b93947d2f23 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 20:17:08 -0400 Subject: [PATCH 044/103] Add missing imports --- tubesync/sync/models/media__tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index d63f7b31..24cbb395 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -1,9 +1,11 @@ import os +from pathlib import Path from common.logger import log from common.errors import ( NoMetadataException, ) from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from ..choices import Val, SourceResolution From f753d25218c7acbc062f4f8ae896b7f37098f926 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 23:00:38 -0400 Subject: [PATCH 045/103] Use `uv` to install `pipenv` --- .github/workflows/ci.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 332801e9..54a0d553 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,9 +93,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install pipenv uv - PIPENV_VERBOSITY=64 pipenv install --system --skip-lock + python -m pip install uv + PIPENV_VERBOSITY=64 uvx --no-config --no-managed-python -v --from 'pipenv==2022.12.19' pipenv install --system --skip-lock - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From 9953092ee33a4eb41350ae97d1694eead7f28d51 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 23:08:15 -0400 Subject: [PATCH 046/103] Also upgrade `pip` --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54a0d553..4a295bf3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,7 +93,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install uv + python -m pip install --upgrade pip uv PIPENV_VERBOSITY=64 uvx --no-config --no-managed-python -v --from 'pipenv==2022.12.19' pipenv install --system --skip-lock - name: Set up Django environment run: | From 31acdc8a73fa2274c959ecc7895f3f3b0f5c9af4 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 23:15:56 -0400 Subject: [PATCH 047/103] Use `uv pip` --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a295bf3..a3939e11 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,8 +93,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip uv - PIPENV_VERBOSITY=64 uvx --no-config --no-managed-python -v --from 'pipenv==2022.12.19' pipenv install --system --skip-lock + python -m pip install uv + uv -v pip install 'pipenv==2022.12.19' + PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From b05b594a3d10bfa395c0d8256f3d457a45959723 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 23:26:57 -0400 Subject: [PATCH 048/103] Use `--system` flag --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a3939e11..8de863b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,7 +94,8 @@ jobs: - name: Install dependencies run: | python -m pip install uv - uv -v pip install 'pipenv==2022.12.19' + uv --no-config --no-managed-python --no-progress -v \ + pip install --system --strict 'pipenv==2022.12.19' PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - name: Set up Django environment run: | From eb26fbca8925daaef5e4f54025dd0a8b2a806563 Mon Sep 17 00:00:00 2001 From: tcely Date: Sat, 17 May 2025 23:31:11 -0400 Subject: [PATCH 049/103] Unpin `pipenv` --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8de863b2..34b73468 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,7 +95,7 @@ jobs: run: | python -m pip install uv uv --no-config --no-managed-python --no-progress -v \ - pip install --system --strict 'pipenv==2022.12.19' + pip install --system --strict pipenv PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - name: Set up Django environment run: | From f6b56c1c4e606c818b189cba8697593d7adfa21c Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 00:19:29 -0400 Subject: [PATCH 050/103] Install with `uv` from requirements.txt --- .github/workflows/ci.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 34b73468..d61d9ece 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,7 +96,12 @@ jobs: python -m pip install uv uv --no-config --no-managed-python --no-progress -v \ pip install --system --strict pipenv - PIPENV_VERBOSITY=64 pipenv install --system --skip-lock + PIPENV_VERBOSITY=64 pipenv lock + PIPENV_VERBOSITY=64 pipenv requirements --hash > requirements.txt + cat -v -n requirements.txt + uv --no-config --no-managed-python --no-progress -v \ + pip install --system --strict --requirements requirements.txt + #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From 3a326a526b3783935e210c31a3af3a850f6e9e94 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 00:29:50 -0400 Subject: [PATCH 051/103] Do not output hashes --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d61d9ece..690354b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -97,7 +97,7 @@ jobs: uv --no-config --no-managed-python --no-progress -v \ pip install --system --strict pipenv PIPENV_VERBOSITY=64 pipenv lock - PIPENV_VERBOSITY=64 pipenv requirements --hash > requirements.txt + PIPENV_VERBOSITY=64 pipenv requirements > requirements.txt cat -v -n requirements.txt uv --no-config --no-managed-python --no-progress -v \ pip install --system --strict --requirements requirements.txt From 9641cc579b5aa03f98dc2f4668c12fe49eac77a5 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 00:57:12 -0400 Subject: [PATCH 052/103] Use `pipenv` for install --- .github/workflows/ci.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 690354b2..d967c585 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,12 +96,13 @@ jobs: python -m pip install uv uv --no-config --no-managed-python --no-progress -v \ pip install --system --strict pipenv - PIPENV_VERBOSITY=64 pipenv lock - PIPENV_VERBOSITY=64 pipenv requirements > requirements.txt - cat -v -n requirements.txt - uv --no-config --no-managed-python --no-progress -v \ - pip install --system --strict --requirements requirements.txt - #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock + #PIPENV_VERBOSITY=64 pipenv lock + #PIPENV_VERBOSITY=64 pipenv requirements > requirements.txt + #cat -v -n requirements.txt + #uv --no-config --no-managed-python --no-progress -v \ + # pip install --system --strict --requirements requirements.txt + PIPENV_VERBOSITY=64 pipenv install --system + pipenv requirements - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From e737a5443fa2b6c6f6e6e88d87cbc4c901e4b652 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:02:52 -0400 Subject: [PATCH 053/103] Use lock separately --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d967c585..a1f8d5ee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,10 +99,11 @@ jobs: #PIPENV_VERBOSITY=64 pipenv lock #PIPENV_VERBOSITY=64 pipenv requirements > requirements.txt #cat -v -n requirements.txt + PIPENV_VERBOSITY=64 pipenv install --system --skip-lock + pipenv lock + pipenv requirements #uv --no-config --no-managed-python --no-progress -v \ # pip install --system --strict --requirements requirements.txt - PIPENV_VERBOSITY=64 pipenv install --system - pipenv requirements - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From a76919aef411dcd64d7ab783aea271566e8ab063 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:09:57 -0400 Subject: [PATCH 054/103] Install with `uv` is faster --- .github/workflows/ci.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a1f8d5ee..0f02ffd1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,14 +96,11 @@ jobs: python -m pip install uv uv --no-config --no-managed-python --no-progress -v \ pip install --system --strict pipenv - #PIPENV_VERBOSITY=64 pipenv lock - #PIPENV_VERBOSITY=64 pipenv requirements > requirements.txt - #cat -v -n requirements.txt - PIPENV_VERBOSITY=64 pipenv install --system --skip-lock pipenv lock - pipenv requirements - #uv --no-config --no-managed-python --no-progress -v \ - # pip install --system --strict --requirements requirements.txt + PIPENV_VERBOSITY=64 pipenv requirements | tee requirements.txt + #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock + uv --no-config --no-managed-python --no-progress -v \ + pip install --system --strict --requirements requirements.txt - name: Set up Django environment run: | cp -v -p tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py From 71cf95cdefe046dfc6c596d5c42c3776f6548f1f Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:14:11 -0400 Subject: [PATCH 055/103] Disable verbose output --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f02ffd1..3aa4d49b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,12 +94,12 @@ jobs: - name: Install dependencies run: | python -m pip install uv - uv --no-config --no-managed-python --no-progress -v \ + uv --no-config --no-managed-python --no-progress \ pip install --system --strict pipenv pipenv lock - PIPENV_VERBOSITY=64 pipenv requirements | tee requirements.txt + pipenv requirements | tee requirements.txt #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock - uv --no-config --no-managed-python --no-progress -v \ + uv --no-config --no-managed-python --no-progress \ pip install --system --strict --requirements requirements.txt - name: Set up Django environment run: | From 3fe1b54b33bec204b808ecbbd99d21490929209c Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:18:49 -0400 Subject: [PATCH 056/103] Try without explicit `pipenv lock` --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3aa4d49b..65c3f87c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,7 +96,7 @@ jobs: python -m pip install uv uv --no-config --no-managed-python --no-progress \ pip install --system --strict pipenv - pipenv lock + #pipenv lock pipenv requirements | tee requirements.txt #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock uv --no-config --no-managed-python --no-progress \ From 294f07d4c467c0ba44bdc42092dd6c37962a2914 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:23:33 -0400 Subject: [PATCH 057/103] The explicit `pipenv lock` is needed --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 65c3f87c..3aa4d49b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,7 +96,7 @@ jobs: python -m pip install uv uv --no-config --no-managed-python --no-progress \ pip install --system --strict pipenv - #pipenv lock + pipenv lock pipenv requirements | tee requirements.txt #PIPENV_VERBOSITY=64 pipenv install --system --skip-lock uv --no-config --no-managed-python --no-progress \ From b9efeeebf280799674f4086293c596868c3736fc Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:39:32 -0400 Subject: [PATCH 058/103] Do not upgrade to Django 5.3 automatically --- Pipfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index bf53b4bf..e4d6d053 100644 --- a/Pipfile +++ b/Pipfile @@ -7,13 +7,13 @@ verify_ssl = true autopep8 = "*" [packages] -django = "*" +django = "~= 5.2.1" django-sass-processor = {extras = ["management-command"], version = "*"} pillow = "*" whitenoise = "*" gunicorn = "*" httptools = "*" -django-background-tasks = ">=1.2.8" +django-background-tasks = ">= 1.2.8" django-basicauth = "*" psycopg = {extras = ["binary", "pool"], version = "*"} mysqlclient = "*" From 7165491b25e30ae44f7656b9b4c255ed6b2f49a8 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 01:42:39 -0400 Subject: [PATCH 059/103] Remove the spaces for better log output --- Pipfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index e4d6d053..b466d84e 100644 --- a/Pipfile +++ b/Pipfile @@ -7,13 +7,13 @@ verify_ssl = true autopep8 = "*" [packages] -django = "~= 5.2.1" +django = "~=5.2.1" django-sass-processor = {extras = ["management-command"], version = "*"} pillow = "*" whitenoise = "*" gunicorn = "*" httptools = "*" -django-background-tasks = ">= 1.2.8" +django-background-tasks = ">=1.2.8" django-basicauth = "*" psycopg = {extras = ["binary", "pool"], version = "*"} mysqlclient = "*" From c69cd7f5823a06a3839dc82568b84a8f13c483cf Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 02:52:15 -0400 Subject: [PATCH 060/103] Fixes for `ruff check` in list-sources.py --- tubesync/sync/management/commands/list-sources.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/management/commands/list-sources.py b/tubesync/sync/management/commands/list-sources.py index 4ee177ae..25eae481 100644 --- a/tubesync/sync/management/commands/list-sources.py +++ b/tubesync/sync/management/commands/list-sources.py @@ -1,7 +1,6 @@ -import os -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError # noqa from common.logger import log -from sync.models import Source, Media, MediaServer +from sync.models import Source class Command(BaseCommand): From 2ece2e7f383dd6d9fc684cbc07e2623eecaf242a Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 03:12:12 -0400 Subject: [PATCH 061/103] Fixes for `ruff check` in reset-tasks.py --- tubesync/sync/management/commands/reset-tasks.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tubesync/sync/management/commands/reset-tasks.py b/tubesync/sync/management/commands/reset-tasks.py index 55436863..318a5b27 100644 --- a/tubesync/sync/management/commands/reset-tasks.py +++ b/tubesync/sync/management/commands/reset-tasks.py @@ -1,14 +1,12 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError # noqa from django.db.transaction import atomic from django.utils.translation import gettext_lazy as _ from background_task.models import Task +from common.logger import log from sync.models import Source from sync.tasks import index_source_task, check_source_directory_exists -from common.logger import log - - class Command(BaseCommand): help = 'Resets all tasks' @@ -31,10 +29,10 @@ class Command(BaseCommand): index_source_task( str(source.pk), repeat=source.index_schedule, + schedule=source.index_schedule, verbose_name=verbose_name.format(source.name), ) - with atomic(durable=True): - for source in Source.objects.all(): # This also chains down to call each Media objects .save() as well source.save() + log.info('Done') From da182070f87867c33b87e2b98ab4ca2bc41eba75 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 03:23:15 -0400 Subject: [PATCH 062/103] Fixes for `ruff check` in delete-source.py --- tubesync/sync/management/commands/delete-source.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tubesync/sync/management/commands/delete-source.py b/tubesync/sync/management/commands/delete-source.py index 42f9d5ac..d9f8f204 100644 --- a/tubesync/sync/management/commands/delete-source.py +++ b/tubesync/sync/management/commands/delete-source.py @@ -1,16 +1,15 @@ -import os import uuid -from django.utils.translation import gettext_lazy as _ from django.core.management.base import BaseCommand, CommandError from django.db.transaction import atomic +from django.utils.translation import gettext_lazy as _ from common.logger import log -from sync.models import Source, Media, MediaServer +from sync.models import Source from sync.tasks import schedule_media_servers_update class Command(BaseCommand): - help = _('Deletes a source by UUID') + help = 'Deletes a source by UUID' def add_arguments(self, parser): parser.add_argument('--source', action='store', required=True, help=_('Source UUID')) From 5dfe52452e2fd45af236e79a8a51461db88eef5d Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 04:30:31 -0400 Subject: [PATCH 063/103] Use the step summary markdown file --- .github/workflows/ci.yaml | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3aa4d49b..a1754c10 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -108,11 +108,30 @@ jobs: cp -v -a -t "${Python3_ROOT_DIR}"/lib/python3.*/site-packages/yt_dlp/ patches/yt_dlp/* cd tubesync && python3 -B manage.py collectstatic --no-input --link - name: Check with ruff - continue-on-error: true + continue-on-error: false run: | - cd tubesync && uvx ruff check --output-format github \ - --target-version py310 --isolated \ - --ignore E701,E722,E731 + cd tubesync + # output formats: + # "full" | "concise" | "grouped" | + # "json" | "junit" | "github" | "gitlab" | + # "pylint" | "azure" + { + echo '# Output from `ruff check` for `tubesync`' + echo '' + echo '## pylint format' + echo '' + uvx --no-config --no-managed-python --no-progress --isolated \ + ruff check --exit-zero \ + --target-version py310 \ + --output-format pylint \ + --extend-select RUF100 \ + --ignore E701,E722,E731 + } >> "${GITHUB_STEP_SUMMARY}" + uvx --no-config --no-managed-python --no-progress --isolated \ + ruff check --exit-zero \ + --target-version py310 \ + --output-format github \ + --ignore E701,E722,E731 - name: Run Django tests run: cd tubesync && python3 -B -W default manage.py test --verbosity=2 From f182a41a219cbb80dd3b6be15f33d8c8f1b03698 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 04:46:14 -0400 Subject: [PATCH 064/103] Loop over output formats --- .github/workflows/ci.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a1754c10..05debc82 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -116,16 +116,26 @@ jobs: # "json" | "junit" | "github" | "gitlab" | # "pylint" | "azure" { - echo '# Output from `ruff check` for `tubesync`' + echo '## Output from `ruff check` for `tubesync`' echo '' - echo '## pylint format' + echo '### Formats' + for fmt in full concise grouped pylint + do + echo '
' + echo ''"${fmt}"'' + echo '#### '"${fmt}"' output format' echo '' + echo '```' uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ --target-version py310 \ - --output-format pylint \ + --output-format "${fmt}" \ --extend-select RUF100 \ --ignore E701,E722,E731 + echo '```' + echo '
' + echo '' + done } >> "${GITHUB_STEP_SUMMARY}" uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ From 9c342c9ccc34c452d6b8406790652a160ec63874 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 04:50:41 -0400 Subject: [PATCH 065/103] Tweak the markdown --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05debc82..0fcf8054 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -119,11 +119,12 @@ jobs: echo '## Output from `ruff check` for `tubesync`' echo '' echo '### Formats' + echo '' for fmt in full concise grouped pylint do echo '
' echo ''"${fmt}"'' - echo '#### '"${fmt}"' output format' + echo '### '"${fmt}"' output format' echo '' echo '```' uvx --no-config --no-managed-python --no-progress --isolated \ From 4d036356e2a2aab2c4f6eee4590fe06c5dcf2452 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 04:58:12 -0400 Subject: [PATCH 066/103] Add a blank line after the summary --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0fcf8054..d7ce5080 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -124,7 +124,8 @@ jobs: do echo '
' echo ''"${fmt}"'' - echo '### '"${fmt}"' output format' + echo '' + echo '#### '"${fmt}"' output format' echo '' echo '```' uvx --no-config --no-managed-python --no-progress --isolated \ From 8e888b4182a1f454fe2a8c76d74623f612298529 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 05:06:32 -0400 Subject: [PATCH 067/103] Only quote `full` output --- .github/workflows/ci.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7ce5080..884ea952 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -127,14 +127,16 @@ jobs: echo '' echo '#### '"${fmt}"' output format' echo '' - echo '```' + case "${fmt}" in (full) echo '```' ;; esac uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ --target-version py310 \ --output-format "${fmt}" \ --extend-select RUF100 \ --ignore E701,E722,E731 - echo '```' + echo '' + case "${fmt}" in (full) echo '```' ;; esac + echo '' echo '
' echo '' done From d502dcd01a58adefd077b7b7d07ea0d3ad5463ed Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 05:10:25 -0400 Subject: [PATCH 068/103] The output was better quoted --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 884ea952..7ae9e61d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -127,7 +127,7 @@ jobs: echo '' echo '#### '"${fmt}"' output format' echo '' - case "${fmt}" in (full) echo '```' ;; esac + echo '```' uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ --target-version py310 \ @@ -135,7 +135,7 @@ jobs: --extend-select RUF100 \ --ignore E701,E722,E731 echo '' - case "${fmt}" in (full) echo '```' ;; esac + echo '```' echo '' echo '
' echo '' From 46f1d7fc89068b9c11c221a4496cac459a934dc6 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 05:18:23 -0400 Subject: [PATCH 069/103] DRY the `ruff check` options --- .github/workflows/ci.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7ae9e61d..22bd31b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -110,6 +110,8 @@ jobs: - name: Check with ruff continue-on-error: false run: | + target_version='py310' + ignore_csv_list='E701,E722,E731' cd tubesync # output formats: # "full" | "concise" | "grouped" | @@ -130,10 +132,10 @@ jobs: echo '```' uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ - --target-version py310 \ + --target-version "${target_version}" \ --output-format "${fmt}" \ --extend-select RUF100 \ - --ignore E701,E722,E731 + --ignore "${ignore_csv_list}" echo '' echo '```' echo '' @@ -143,9 +145,9 @@ jobs: } >> "${GITHUB_STEP_SUMMARY}" uvx --no-config --no-managed-python --no-progress --isolated \ ruff check --exit-zero \ - --target-version py310 \ + --target-version "${target_version}" \ --output-format github \ - --ignore E701,E722,E731 + --ignore "${ignore_csv_list}" - name: Run Django tests run: cd tubesync && python3 -B -W default manage.py test --verbosity=2 From 7a53e363b2ae3fdbe2d753c01d154b97aac16f52 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 05:24:36 -0400 Subject: [PATCH 070/103] Fixes for `ruff check` in sync-missing-metadata.py --- tubesync/sync/management/commands/sync-missing-metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tubesync/sync/management/commands/sync-missing-metadata.py b/tubesync/sync/management/commands/sync-missing-metadata.py index 21b25c52..863f83f9 100644 --- a/tubesync/sync/management/commands/sync-missing-metadata.py +++ b/tubesync/sync/management/commands/sync-missing-metadata.py @@ -1,6 +1,5 @@ -import os from shutil import copyfile -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError # noqa from django.db.models import Q from common.logger import log from sync.models import Source, Media From 87bcfb90940c86b077d223ef79c87dc86a005623 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 05:51:31 -0400 Subject: [PATCH 071/103] Fixes for `ruff check` in youtube-dl-info.py --- tubesync/sync/management/commands/youtube-dl-info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/management/commands/youtube-dl-info.py b/tubesync/sync/management/commands/youtube-dl-info.py index 32a47402..c8099765 100644 --- a/tubesync/sync/management/commands/youtube-dl-info.py +++ b/tubesync/sync/management/commands/youtube-dl-info.py @@ -1,7 +1,7 @@ import json -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError # noqa from sync.youtube import get_media_info -from common.utils import json_serial +from common.json import JSONEncoder class Command(BaseCommand): @@ -15,6 +15,6 @@ class Command(BaseCommand): url = options['url'] self.stdout.write(f'Showing information for URL: {url}') info = get_media_info(url) - d = json.dumps(info, indent=4, sort_keys=True, default=json_serial) + d = json.dumps(info, indent=4, sort_keys=True, cls=JSONEncoder) self.stdout.write(d) self.stdout.write('Done') From 9a173dc4775919f2e81f52fbf7f5682966dfa73d Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:08:49 -0400 Subject: [PATCH 072/103] Use `JSONEncoder` in media.py --- tubesync/sync/models/media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/models/media.py b/tubesync/sync/models/media.py index 4af39c7a..11b30f80 100644 --- a/tubesync/sync/models/media.py +++ b/tubesync/sync/models/media.py @@ -15,6 +15,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from common.logger import log from common.errors import NoFormatException +from common.json import JSONEncoder from common.utils import ( clean_filename, clean_emoji, ) @@ -577,14 +578,13 @@ class Media(models.Model): def metadata_dumps(self, arg_dict=dict()): - from common.utils import json_serial fallback = dict() try: fallback.update(self.new_metadata.with_formats) except ObjectDoesNotExist: pass data = arg_dict or fallback - return json.dumps(data, separators=(',', ':'), default=json_serial) + return json.dumps(data, separators=(',', ':'), cls=JSONEncoder) def metadata_loads(self, arg_str='{}'): From 3abab98f64f113993cd023a9058b57c01f978757 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:11:16 -0400 Subject: [PATCH 073/103] Remove `json_serial` from utils.py --- tubesync/common/utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tubesync/common/utils.py b/tubesync/common/utils.py index 5fc515df..020035b8 100644 --- a/tubesync/common/utils.py +++ b/tubesync/common/utils.py @@ -169,14 +169,6 @@ def clean_emoji(s): return emoji.replace_emoji(s) -def json_serial(obj): - if isinstance(obj, datetime): - return obj.isoformat() - if isinstance(obj, LazyList): - return list(obj) - raise TypeError(f'Type {type(obj)} is not json_serial()-able') - - def time_func(func): def wrapper(*args, **kwargs): start = time.perf_counter() From 1ba4bca58b4563fffdc88cdf199f84aea20a6bc7 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:16:06 -0400 Subject: [PATCH 074/103] Add `json_serial` to json.py --- tubesync/common/json.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tubesync/common/json.py b/tubesync/common/json.py index e8a22e1c..5a56a019 100644 --- a/tubesync/common/json.py +++ b/tubesync/common/json.py @@ -1,4 +1,6 @@ +from datetime import datetime from django.core.serializers.json import DjangoJSONEncoder +from yt_dlp.utils import LazyList class JSONEncoder(DjangoJSONEncoder): @@ -14,3 +16,11 @@ class JSONEncoder(DjangoJSONEncoder): return list(iterable) return super().default(obj) + +def json_serial(obj): + if isinstance(obj, datetime): + return obj.isoformat() + if isinstance(obj, LazyList): + return list(obj) + raise TypeError(f'Type {type(obj)} is not json_serial()-able') + From cc0ed6fd473047c2f661dfb50f668dba7103d67d Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:18:41 -0400 Subject: [PATCH 075/103] Remove unused imports from utils.py --- tubesync/common/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tubesync/common/utils.py b/tubesync/common/utils.py index 020035b8..8f7afc2c 100644 --- a/tubesync/common/utils.py +++ b/tubesync/common/utils.py @@ -6,10 +6,8 @@ import os import pstats import string import time -from datetime import datetime from django.core.paginator import Paginator from urllib.parse import urlunsplit, urlencode, urlparse -from yt_dlp.utils import LazyList from .errors import DatabaseConnectionError From aa8804bb7d58261111b30eef5764f7e4be47bebb Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:25:23 -0400 Subject: [PATCH 076/103] Fixes for `ruff check` in import-existing-media.py --- tubesync/sync/management/commands/import-existing-media.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/management/commands/import-existing-media.py b/tubesync/sync/management/commands/import-existing-media.py index 3813b497..c05c630a 100644 --- a/tubesync/sync/management/commands/import-existing-media.py +++ b/tubesync/sync/management/commands/import-existing-media.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError # noqa from common.logger import log from common.timestamp import timestamp_to_datetime from sync.choices import FileExtension @@ -18,7 +18,7 @@ class Command(BaseCommand): dirmap = {} for s in Source.objects.all(): dirmap[str(s.directory_path)] = s - log.info(f'Scanning sources...') + log.info('Scanning sources...') file_extensions = list(FileExtension.values) + self.extra_extensions for sourceroot, source in dirmap.items(): media = list(Media.objects.filter(source=source, downloaded=False, From 226a8d6385c1027044935320d3c2ed6fe3792216 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:31:24 -0400 Subject: [PATCH 077/103] Fixes for `ruff check` in models/_private.py --- tubesync/sync/models/_private.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models/_private.py b/tubesync/sync/models/_private.py index 96539dbe..5ec14d7c 100644 --- a/tubesync/sync/models/_private.py +++ b/tubesync/sync/models/_private.py @@ -1,4 +1,4 @@ -from ..choices import Val, YouTube_SourceType +from ..choices import Val, YouTube_SourceType # noqa _srctype_dict = lambda n: dict(zip( YouTube_SourceType.values, (n,) * len(YouTube_SourceType.values) )) From bb6d5c0a48cbb2cf8e2a26f3a917681467c62cc7 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:43:34 -0400 Subject: [PATCH 078/103] Fixes for `ruff check` in tests.py --- tubesync/sync/tests.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 24f0d092..9c510838 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -6,7 +6,6 @@ import logging -import os from datetime import datetime, timedelta from pathlib import Path from urllib.parse import urlsplit @@ -1822,13 +1821,13 @@ class TasksTestCase(TestCase): now = timezone.now() - m11 = Media.objects.create(source=src1, downloaded=True, key='a11', download_date=now - timedelta(days=5)) - m12 = Media.objects.create(source=src1, downloaded=True, key='a12', download_date=now - timedelta(days=25)) - m13 = Media.objects.create(source=src1, downloaded=False, key='a13') + m11 = Media.objects.create(source=src1, downloaded=True, key='a11', download_date=now - timedelta(days=5)) # noqa + m12 = Media.objects.create(source=src1, downloaded=True, key='a12', download_date=now - timedelta(days=25)) # noqa + m13 = Media.objects.create(source=src1, downloaded=False, key='a13') # noqa - m21 = Media.objects.create(source=src2, downloaded=True, key='a21', download_date=now - timedelta(days=5)) - m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25)) - m23 = Media.objects.create(source=src2, downloaded=False, key='a23') + m21 = Media.objects.create(source=src2, downloaded=True, key='a21', download_date=now - timedelta(days=5)) # noqa + m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25)) # noqa + m23 = Media.objects.create(source=src2, downloaded=False, key='a23') # noqa self.assertEqual(src1.media_source.all().count(), 3) self.assertEqual(src2.media_source.all().count(), 3) From 0f27ffe29e8484f6f046d9f5990cf64f864de3b3 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:50:13 -0400 Subject: [PATCH 079/103] Fixes for `ruff check` in settings.py --- tubesync/tubesync/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/tubesync/settings.py b/tubesync/tubesync/settings.py index 7f5922ae..37f72de3 100644 --- a/tubesync/tubesync/settings.py +++ b/tubesync/tubesync/settings.py @@ -198,7 +198,7 @@ RENAME_SOURCES = None # You have been warned! try: - from .local_settings import * + from .local_settings import * # noqa except ImportError as e: import sys sys.stderr.write(f'Unable to import local_settings: {e}\n') @@ -222,5 +222,5 @@ if BACKGROUND_TASK_ASYNC_THREADS > MAX_BACKGROUND_TASK_ASYNC_THREADS: BACKGROUND_TASK_ASYNC_THREADS = MAX_BACKGROUND_TASK_ASYNC_THREADS -from .dbutils import patch_ensure_connection +from .dbutils import patch_ensure_connection # noqa patch_ensure_connection() From a4c763d723380764f0c15271b80f9329b6af9b56 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 06:55:12 -0400 Subject: [PATCH 080/103] Fixes for `ruff check` in 0013_fix_elative_media_file.py --- tubesync/sync/migrations/0013_fix_elative_media_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/migrations/0013_fix_elative_media_file.py b/tubesync/sync/migrations/0013_fix_elative_media_file.py index c9eee22e..2f1ac385 100644 --- a/tubesync/sync/migrations/0013_fix_elative_media_file.py +++ b/tubesync/sync/migrations/0013_fix_elative_media_file.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.12 on 2022-04-06 06:19 from django.conf import settings -from django.db import migrations, models +from django.db import migrations def fix_media_file(apps, schema_editor): From 853c4878b5c76e5da6a93be2c6451fad8d348b40 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 07:00:27 -0400 Subject: [PATCH 081/103] Fixes for `ruff check` in 0011_auto_20220201_1654.py --- tubesync/sync/migrations/0011_auto_20220201_1654.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tubesync/sync/migrations/0011_auto_20220201_1654.py b/tubesync/sync/migrations/0011_auto_20220201_1654.py index 96d9f4a7..51641ece 100644 --- a/tubesync/sync/migrations/0011_auto_20220201_1654.py +++ b/tubesync/sync/migrations/0011_auto_20220201_1654.py @@ -1,8 +1,6 @@ # Generated by Django 3.2.11 on 2022-02-01 16:54 -import django.core.files.storage from django.db import migrations, models -import sync.models class Migration(migrations.Migration): From a9508fa2803052cc20e0a21fb4f9d1ba778fd80c Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 07:21:13 -0400 Subject: [PATCH 082/103] Update tests.py --- tubesync/sync/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 9c510838..47089673 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -1826,7 +1826,7 @@ class TasksTestCase(TestCase): m13 = Media.objects.create(source=src1, downloaded=False, key='a13') # noqa m21 = Media.objects.create(source=src2, downloaded=True, key='a21', download_date=now - timedelta(days=5)) # noqa - m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25)) # noqa + m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25)) m23 = Media.objects.create(source=src2, downloaded=False, key='a23') # noqa self.assertEqual(src1.media_source.all().count(), 3) From 0e720f90423f5917edc7150b4cbdf139dbdbf96e Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 21:45:24 -0400 Subject: [PATCH 083/103] Use the `list.append` function, which is safe for strings --- tubesync/sync/youtube.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tubesync/sync/youtube.py b/tubesync/sync/youtube.py index 9814db24..c795261a 100644 --- a/tubesync/sync/youtube.py +++ b/tubesync/sync/youtube.py @@ -326,13 +326,10 @@ def download_media( final_path = Path(output_file).resolve(strict=False) expected_file = shell_quote(str(final_path)) cmds = pp_opts.exec_cmd.get('after_move', list()) - # It is important that we use a tuple for strings. - # Otherwise, list adds each character instead. - # That last comma is really necessary! - cmds += ( + cmds.append( f'test -f {expected_file} || ' 'mv -T -u -- %(filepath,_filename|)q ' - f'{expected_file}', + f'{expected_file}' ) # assignment is the quickest way to cover both 'get' cases pp_opts.exec_cmd['after_move'] = cmds @@ -387,7 +384,7 @@ def download_media( youtube_ea_dict = ytopts['extractor_args'].get('youtube', dict()) formats_list = youtube_ea_dict.get('formats', list()) if 'missing_pot' not in formats_list: - formats_list += ('missing_pot',) + formats_list.append('missing_pot') youtube_ea_dict.update({ 'formats': formats_list, }) From 971cea5c0eef12e4baa829985e43ec5b653866f9 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 18 May 2025 22:26:06 -0400 Subject: [PATCH 084/103] Use the less ambiguous `list.extend` function --- tubesync/sync/models/source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 74f75278..063907f3 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -527,8 +527,8 @@ class Source(db.models.Model): days = timezone.timedelta(seconds=self.download_cap).days response = indexer(self.get_index_url(type=type), days=days) if not isinstance(response, dict): - return [] - entries = response.get('entries', []) + return list() + entries = response.get('entries', list()) return entries def index_media(self): @@ -537,11 +537,11 @@ class Source(db.models.Model): ''' entries = list() if self.index_videos: - entries += self.get_index('videos') + entries.extend(self.get_index('videos')) # Playlists do something different that I have yet to figure out if not self.is_playlist: if self.index_streams: - entries += self.get_index('streams') + entries.extend(self.get_index('streams')) if settings.MAX_ENTRIES_PROCESSING: entries = entries[:settings.MAX_ENTRIES_PROCESSING] From 8ba9ce40782e231f377148ae0504e319f77f9e9f Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 00:10:13 -0400 Subject: [PATCH 085/103] Use a queue to avoid list memory performance --- tubesync/sync/models/source.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 063907f3..ebe90767 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -1,6 +1,7 @@ import os import re import uuid +from collections import deque from pathlib import Path from django import db from django.conf import settings @@ -533,17 +534,20 @@ class Source(db.models.Model): def index_media(self): ''' - Index the media source returning a list of media metadata as dicts. + Index the media source returning a queue of media metadata as dicts. ''' - entries = list() + entries = deque(list(), settings.get('MAX_ENTRIES_PROCESSING', 0) or None) if self.index_videos: entries.extend(self.get_index('videos')) + # Playlists do something different that I have yet to figure out if not self.is_playlist: if self.index_streams: - entries.extend(self.get_index('streams')) + streams = self.get_index('streams') + # do not allow streams to consume all of the queue + if entries.maxlen and entries.maxlen <= len(streams): + streams = streams[-1 * ( entries.maxlen // 2 ) :] + entries.extend(reversed(streams)) - if settings.MAX_ENTRIES_PROCESSING: - entries = entries[:settings.MAX_ENTRIES_PROCESSING] return entries From c7c37b12d89a1044665c213b114d55b05fd5e82e Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 00:12:47 -0400 Subject: [PATCH 086/103] Use `queue` for better readability --- tubesync/sync/models/source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index ebe90767..a739936a 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -1,7 +1,7 @@ import os import re import uuid -from collections import deque +from collections import deque as queue from pathlib import Path from django import db from django.conf import settings @@ -536,7 +536,7 @@ class Source(db.models.Model): ''' Index the media source returning a queue of media metadata as dicts. ''' - entries = deque(list(), settings.get('MAX_ENTRIES_PROCESSING', 0) or None) + entries = queue(list(), settings.get('MAX_ENTRIES_PROCESSING', 0) or None) if self.index_videos: entries.extend(self.get_index('videos')) From e84643b95e517a3d28f0020741763442fdcbb4f7 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 01:44:49 -0400 Subject: [PATCH 087/103] Handle unbalanced channels better --- tubesync/sync/models/source.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index a739936a..04fe84d4 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -544,10 +544,15 @@ class Source(db.models.Model): if not self.is_playlist: if self.index_streams: streams = self.get_index('streams') - # do not allow streams to consume all of the queue - if entries.maxlen and entries.maxlen <= len(streams): - streams = streams[-1 * ( entries.maxlen // 2 ) :] - entries.extend(reversed(streams)) + if entries.maxlen is None or 0 == len(entries): + entries.extend(streams) + else: + # share the queue between streams and videos + allowed_streams = max( + entries.maxlen // 2, + entries.maxlen - len(entries), + ) + entries.extend(streams[-1 * allowed_streams :]) return entries From 2483cda1af9db626ba912ea9b4a4a838c5752079 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 05:54:23 -0400 Subject: [PATCH 088/103] Remove the unused `getpot_bgutil_script` plugin --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index 6ef178c5..c878890e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -410,6 +410,12 @@ RUN --mount=type=tmpfs,target=/cache \ PIPENV_VERBOSITY=64 \ PYTHONPYCACHEPREFIX=/cache/pycache \ pipenv install --system --skip-lock && \ + # remove the getpot_bgutil_script plugin + find /usr/local/lib \ + -name 'getpot_bgutil_script.py' \ + -path '*/yt_dlp_plugins/extractor/getpot_bgutil_script.py' \ + -type f -print -delete \ + && \ # Clean up apt-get -y autoremove --purge \ default-libmysqlclient-dev \ From 01fcf5ec66475238918eacfff7bc0b759d184882 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 07:17:56 -0400 Subject: [PATCH 089/103] Reverse the lists when adding to the queue The default sorting appears to be newest first. We want the oldest to be dropped from the queue, by any limits instead. --- tubesync/sync/models/source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 04fe84d4..776ddfe8 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -538,21 +538,21 @@ class Source(db.models.Model): ''' entries = queue(list(), settings.get('MAX_ENTRIES_PROCESSING', 0) or None) if self.index_videos: - entries.extend(self.get_index('videos')) + entries.extend(reversed(self.get_index('videos'))) # Playlists do something different that I have yet to figure out if not self.is_playlist: if self.index_streams: streams = self.get_index('streams') if entries.maxlen is None or 0 == len(entries): - entries.extend(streams) + entries.extend(reversed(streams)) else: # share the queue between streams and videos allowed_streams = max( entries.maxlen // 2, entries.maxlen - len(entries), ) - entries.extend(streams[-1 * allowed_streams :]) + entries.extend(reversed(streams[-1 * allowed_streams :])) return entries From 4cbcd7688536f6a2e19bfa4d79fcd971e0a232d5 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 07:36:37 -0400 Subject: [PATCH 090/103] Adjust for a queue instead of a list --- tubesync/sync/tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 0467a4fd..b192ccfb 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -316,7 +316,10 @@ def index_source_task(source_id): end=task.verbose_name.find('Index'), ) tvn_format = '{:,}' + f'/{num_videos:,}' - for vn, video in enumerate(videos, start=1): + vn = 0 + while len(videos) > 0: + vn += 1 + video = videos.popleft() # Create or update each video as a Media object key = video.get(source.key_field, None) if not key: From 8109d6a836c4b1c0ff16f01d874621105cc5c648 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 08:09:56 -0400 Subject: [PATCH 091/103] Use `getattr` not `dict.get` for `settings` --- tubesync/sync/models/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index 776ddfe8..82486521 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -536,7 +536,7 @@ class Source(db.models.Model): ''' Index the media source returning a queue of media metadata as dicts. ''' - entries = queue(list(), settings.get('MAX_ENTRIES_PROCESSING', 0) or None) + entries = queue(list(), getattr(settings, 'MAX_ENTRIES_PROCESSING', 0) or None) if self.index_videos: entries.extend(reversed(self.get_index('videos'))) From ed55710f26e08657f206aef48080eb2f28a5319b Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 08:54:21 -0400 Subject: [PATCH 092/103] Create a set of keys for `cleanup_removed_media` --- tubesync/sync/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index b192ccfb..6d0727c0 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -255,7 +255,7 @@ def cleanup_old_media(): schedule_media_servers_update() -def cleanup_removed_media(source, videos): +def cleanup_removed_media(source, video_keys): if not source.delete_removed_media: return log.info(f'Cleaning up media no longer in source: {source}') @@ -265,8 +265,7 @@ def cleanup_removed_media(source, videos): source=source, ) for media in qs_gen(mqs): - matching_source_item = [video['id'] for video in videos if video['id'] == media.key] - if not matching_source_item: + if media.key not in video_keys: log.info(f'{media.name} is no longer in source, removing') with atomic(): media.delete() @@ -317,6 +316,7 @@ def index_source_task(source_id): ) tvn_format = '{:,}' + f'/{num_videos:,}' vn = 0 + video_keys = set() while len(videos) > 0: vn += 1 video = videos.popleft() @@ -325,6 +325,7 @@ def index_source_task(source_id): if not key: # Video has no unique key (ID), it can't be indexed continue + video_keys.add(key) update_task_status(task, tvn_format.format(vn)) # media, new_media = Media.objects.get_or_create(key=key, source=source) try: @@ -379,7 +380,7 @@ def index_source_task(source_id): # Reset task.verbose_name to the saved value update_task_status(task, None) # Cleanup of media no longer available from the source - cleanup_removed_media(source, videos) + cleanup_removed_media(source, video_keys) videos = video = None From d05b53919ca1a6f9fbfbeda009eb1db941d07531 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 11:17:39 -0400 Subject: [PATCH 093/103] Wait for HTTP 429 errors --- tubesync/sync/tasks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 0467a4fd..ba9f0261 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -235,6 +235,21 @@ def schedule_media_servers_update(): ) +def wait_for_errors(media, /, tn='sync.tasks.download_media_metadata'): + window = timezone.timedelta(hours=3) + timezone.now() + tqs = Task.objects.filter( + task_name=tn, + attempts__gt=0, + locked_at__isnull=True, + run_at__lte=window, + last_error__contains='HTTPError 429: Too Many Requests', + ) + task = get_first_task(tn, instance=media) + update_task_status(task, 'paused (429)') + time.sleep(10 * tqs.count()) + update_task_status(task, None) + + def cleanup_old_media(): with atomic(): for source in qs_gen(Source.objects.filter(delete_old_media=True, days_to_keep__gt=0)): @@ -467,6 +482,7 @@ def download_media_metadata(media_id): log.info(f'Task for ID: {media_id} / {media} skipped, due to task being manually skipped.') return source = media.source + wait_for_errors(media) try: metadata = media.index_metadata() except YouTubeError as e: From 1be1e583e56ac16ca3bb18661cadec1276181580 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 11:32:56 -0400 Subject: [PATCH 094/103] Use `hasattr` for `update_task_status` --- tubesync/sync/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index ba9f0261..2942f37e 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -122,7 +122,7 @@ def get_error_message(task): def update_task_status(task, status): if not task: return False - if not task._verbose_name: + if not hasattr(task, '_verbose_name'): task._verbose_name = remove_enclosed( task.verbose_name, '[', ']', ' ', ) From 8080f690769d50816af6aad66adb2d5c0bbf7ae9 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 11:47:52 -0400 Subject: [PATCH 095/103] Remove an unnecessary condition --- tubesync/sync/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 2942f37e..8b39434b 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -848,8 +848,7 @@ def wait_for_media_premiere(media_id): if hours: task = get_media_premiere_task(media_id) - if task: - update_task_status(task, f'available in {hours} hours') + update_task_status(task, f'available in {hours} hours') save_model(media) From be6a51b05273c301a31e34dc77ff8affcd8a37db Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 15:28:27 -0400 Subject: [PATCH 096/103] The check list needs to pass if nothing failed --- tubesync/sync/models/media__tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tubesync/sync/models/media__tasks.py b/tubesync/sync/models/media__tasks.py index d63f7b31..e8b33235 100644 --- a/tubesync/sync/models/media__tasks.py +++ b/tubesync/sync/models/media__tasks.py @@ -1,9 +1,11 @@ import os +from pathlib import Path from common.logger import log from common.errors import ( NoMetadataException, ) from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from ..choices import Val, SourceResolution @@ -47,6 +49,7 @@ def download_checklist(self, skip_checks=False): f'the source has a download cap and the media is now too old, ' f'not downloading') return False + return True def download_finished(self, format_str, container, downloaded_filepath=None): From b1267a3e59981cf3ee32292ff754ff3add572dc2 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 15:34:13 -0400 Subject: [PATCH 097/103] Reschedule the task when the checklist failed --- tubesync/sync/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 0467a4fd..2736fdc7 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -615,7 +615,7 @@ def download_media(media_id, override=False): raise InvalidTaskError(_('no such media')) from e else: if not media.download_checklist(override): - return + raise Exception('media download not ready') filepath = media.filepath container = format_str = None From 0aac8c27ed4cec619acbd10678c3ec05901dfcd0 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 15:40:54 -0400 Subject: [PATCH 098/103] Raising an exception here is not needed --- tubesync/sync/tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 2736fdc7..00f4ffd3 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -615,7 +615,9 @@ def download_media(media_id, override=False): raise InvalidTaskError(_('no such media')) from e else: if not media.download_checklist(override): - raise Exception('media download not ready') + # any condition that needs ro reschedule the task + # should raise an exception to avoid this + return filepath = media.filepath container = format_str = None From e5347d485feffa06d1af3dc9e318512798f83eb7 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 19 May 2025 15:43:56 -0400 Subject: [PATCH 099/103] fixup: spelling in the comment --- tubesync/sync/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 00f4ffd3..7ce6df42 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -615,7 +615,7 @@ def download_media(media_id, override=False): raise InvalidTaskError(_('no such media')) from e else: if not media.download_checklist(override): - # any condition that needs ro reschedule the task + # any condition that needs to reschedule the task # should raise an exception to avoid this return From e135da45cab33954ffb86cff19a02887da7c2c1e Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 20 May 2025 02:51:23 -0400 Subject: [PATCH 100/103] This can be any model, not just media --- tubesync/sync/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 61cb52a0..78cc97b1 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -233,7 +233,7 @@ def schedule_media_servers_update(): ) -def wait_for_errors(media, /, tn='sync.tasks.download_media_metadata'): +def wait_for_errors(model, /, tn='sync.tasks.download_media_metadata'): window = timezone.timedelta(hours=3) + timezone.now() tqs = Task.objects.filter( task_name=tn, @@ -242,7 +242,7 @@ def wait_for_errors(media, /, tn='sync.tasks.download_media_metadata'): run_at__lte=window, last_error__contains='HTTPError 429: Too Many Requests', ) - task = get_first_task(tn, instance=media) + task = get_first_task(tn, instance=model) update_task_status(task, 'paused (429)') time.sleep(10 * tqs.count()) update_task_status(task, None) From 3c1cf294ebeec0996909f48697fa4323846608fe Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 20 May 2025 02:57:02 -0400 Subject: [PATCH 101/103] Streams are newest first also Keep the beginning when limited: Newest, ..., Oldest Reverse direction when adding to the queue: Oldest, ..., Newest This way when it is limited by queue size, we keep the newest streams for indexing. --- tubesync/sync/models/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models/source.py b/tubesync/sync/models/source.py index d85334f0..07ade020 100644 --- a/tubesync/sync/models/source.py +++ b/tubesync/sync/models/source.py @@ -552,7 +552,7 @@ class Source(db.models.Model): entries.maxlen // 2, entries.maxlen - len(entries), ) - entries.extend(reversed(streams[-1 * allowed_streams :])) + entries.extend(reversed(streams[: allowed_streams])) return entries From 52a1140f5d944af62a8b0b56b763b4f60556875c Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 20 May 2025 03:25:53 -0400 Subject: [PATCH 102/103] Wait for HTTP 429 for media and metadata download tasks --- tubesync/sync/tasks.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 78cc97b1..52c07533 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -233,10 +233,17 @@ def schedule_media_servers_update(): ) -def wait_for_errors(model, /, tn='sync.tasks.download_media_metadata'): +def wait_for_errors(model, /, *, task_name=None): + if task_name is None: + task_name=tuple(( + 'sync.tasks.download_media', + 'sync.tasks.download_media_metadata', + )) + elif isinstance(task_name, str): + task_name = tuple((task_name,)) window = timezone.timedelta(hours=3) + timezone.now() tqs = Task.objects.filter( - task_name=tn, + task_name__in=task_name, attempts__gt=0, locked_at__isnull=True, run_at__lte=window, @@ -484,7 +491,7 @@ def download_media_metadata(media_id): log.info(f'Task for ID: {media_id} / {media} skipped, due to task being manually skipped.') return source = media.source - wait_for_errors(media) + wait_for_errors(media, task_name='sync.tasks.download_media_metadata') try: metadata = media.index_metadata() except YouTubeError as e: @@ -637,6 +644,7 @@ def download_media(media_id, override=False): # should raise an exception to avoid this return + wait_for_errors(media, task_name='sync.tasks.download_media') filepath = media.filepath container = format_str = None log.info(f'Downloading media: {media} (UUID: {media.pk}) to: "{filepath}"') From c539487b4bd63e57cb709aef42bbe20c9340cb3f Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 20 May 2025 03:43:12 -0400 Subject: [PATCH 103/103] Handle multiple task names --- tubesync/sync/tasks.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 52c07533..466e41d3 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -241,6 +241,11 @@ def wait_for_errors(model, /, *, task_name=None): )) elif isinstance(task_name, str): task_name = tuple((task_name,)) + tasks = list() + for tn in task_name: + ft = get_first_task(tn, instance=model) + if ft: + tasks.append(ft) window = timezone.timedelta(hours=3) + timezone.now() tqs = Task.objects.filter( task_name__in=task_name, @@ -249,10 +254,12 @@ def wait_for_errors(model, /, *, task_name=None): run_at__lte=window, last_error__contains='HTTPError 429: Too Many Requests', ) - task = get_first_task(tn, instance=model) - update_task_status(task, 'paused (429)') + for task in tasks: + update_task_status(task, 'paused (429)') + log.info(f'waiting for errors: 429 ({tqs.count()}): {model}') time.sleep(10 * tqs.count()) - update_task_status(task, None) + for task in tasks: + update_task_status(task, None) def cleanup_old_media():