30 Commits

Author SHA1 Message Date
woozu-shin
0b13065c9d [NO-ISSUE] Add lightweight option 2024-02-21 12:53:53 +09:00
meeb
7aa9c0ec8a bump to 0.13.3 2023-11-30 18:58:29 +11:00
meeb
e54a762a7b rework skip logic check, prevent race condition between metadata downloading and upload date being checked, resolves #440, #183, related to #438 2023-11-30 18:52:32 +11:00
meeb
512b70adad toggle logging verbosity based on settings.DEBUG 2023-11-30 18:50:22 +11:00
meeb
6c21ff15ab stopcontainer helper 2023-11-30 18:49:58 +11:00
meeb
adf26cb4e3 bump ffmpeg to autobuild-2023-11-29-14-19 2023-11-30 18:49:50 +11:00
meeb
45c12561ba Merge pull request #438 from locke4/main
Fix signals.py mistake
2023-11-29 04:05:13 +11:00
locke4
2d6f485a5d Update signals.py 2023-11-28 08:48:31 +00:00
meeb
33b471175a Merge pull request #425 from locke4/main
Add support for regex video title filtering
2023-11-20 16:53:58 +11:00
meeb
7f4e8586b7 Merge pull request #435 from klinker41/patch-1
Update other-database-backends.md
2023-11-20 16:51:00 +11:00
Jake Klinker
bab4b9b056 Update other-database-backends.md
Add documentation about how to use a docker compose postgres container and connect it to tubesync. This seems like a fairly basic use case that many users would want to implement, given the large performance benefits it brings.
2023-11-19 10:23:07 -07:00
meeb
30c2127271 bump ffmpeg to 2023-11-14 and yt-dlp to 2023.11.16 2023-11-16 18:54:57 +11:00
locke4
d1cb7ef76c Delete tubesync/sync/migrations/0020_auto_20231024_1812.py 2023-10-24 19:26:50 +01:00
locke4
1fd4f87c53 Merge pull request #8 from locke4/fix-pagenums
Ran makemigrations
2023-10-24 19:25:52 +01:00
locke4
cf06f4cbc2 Merge pull request #7 from locke4/locke4-patch-2
Updated according to comments on PR
2023-10-24 18:38:17 +01:00
locke4
0523f481d2 Updated according to comments on PR
Fixed whitespace

Update tests.py

Ran makemigrations

Update models.py

Update tests.py

Update models.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update models.py

Update tests.py

Update tests.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update tests.py

Update signals.py

Update tasks.py

Update signals.py

Update models.py

Update tasks.py

Update signals.py

Update tasks.py

Update models.py
2023-10-24 18:37:09 +01:00
locke4
aa4bd4ec26 Ran makemigrations 2023-10-24 18:17:56 +01:00
locke4
96d9ee93ef Merge pull request #6 from locke4/fix-pagenums
Fix pagenums for "only_skipped" query param
2023-10-22 13:39:11 +01:00
locke4
8240c49d5c Update ci.yaml 2023-10-22 02:42:57 +01:00
locke4
0c5e3d3818 Update media.html 2023-10-22 02:30:24 +01:00
locke4
22edd1bbda Update pagination.html 2023-10-22 02:25:19 +01:00
locke4
fea0bb191e Fix typo 2023-10-21 21:23:57 +01:00
locke4
0f65a4027a Add support for regex filters on video names
Update views.py
Update tests.py
Update source.html
Update tasks.py
Update signals.py
Update 0001_initial.py
Update models.py
Update models.py
Update tests.py
2023-10-21 21:07:15 +01:00
meeb
5cac374486 Merge pull request #420 from sparklesmcfadden/delete-removed-media
Adds workflow to delete local media that no longer exists in the source
2023-10-21 14:31:39 +11:00
meeb
69efc9298d Merge pull request #423 from ltomes/patch-1
Update other-database-backends.md
2023-10-21 14:30:06 +11:00
Levi Tomes
1be8dff769 Update other-database-backends.md
django-admin only ran the loaddata for me with the - before the format flag.
2023-10-20 18:22:40 -05:00
cavanfarrell
350e544594 Fixes formatting 2023-10-20 10:25:20 -05:00
cavanfarrell
0542c734e5 Adds workflow to delete local media that no longer exists in the source 2023-10-20 10:19:57 -05:00
meeb
42b337c408 bump ffmpeg to autobuild-2023-10-11-14-20 2023-10-12 15:50:38 +11:00
meeb
2f82f8c599 fix tests 2023-10-12 15:44:51 +11:00
23 changed files with 329 additions and 34 deletions

View File

@@ -4,6 +4,7 @@ env:
IMAGE_NAME: tubesync IMAGE_NAME: tubesync
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tubesync.iml" filepath="$PROJECT_DIR$/.idea/tubesync.iml" />
</modules>
</component>
</project>

20
.idea/tubesync.iml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/tubesync/common/templates" />
</list>
</option>
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -2,8 +2,8 @@ FROM debian:bookworm-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG S6_VERSION="3.1.5.0" ARG S6_VERSION="3.1.5.0"
ARG FFMPEG_DATE="autobuild-2023-09-24-14-11" ARG FFMPEG_DATE="autobuild-2023-11-29-14-19"
ARG FFMPEG_VERSION="112171-g13a3e2a9b4" ARG FFMPEG_VERSION="112875-g47e214245b"
ENV DEBIAN_FRONTEND="noninteractive" \ ENV DEBIAN_FRONTEND="noninteractive" \
HOME="/root" \ HOME="/root" \
@@ -27,8 +27,8 @@ RUN export ARCH=$(case ${TARGETPLATFORM:-linux/amd64} in \
"linux/arm64") echo "https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-aarch64.tar.xz" ;; \ "linux/arm64") echo "https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-aarch64.tar.xz" ;; \
*) echo "" ;; esac) && \ *) echo "" ;; esac) && \
export FFMPEG_EXPECTED_SHA256=$(case ${TARGETPLATFORM:-linux/amd64} in \ export FFMPEG_EXPECTED_SHA256=$(case ${TARGETPLATFORM:-linux/amd64} in \
"linux/amd64") echo "71cd08ed38c33ff2625dcca68d05efda090bdae455625d3bb1e4be4a53bf7c11" ;; \ "linux/amd64") echo "36bac8c527bf390603416f749ab0dd860142b0a66f0865b67366062a9c286c8b" ;; \
"linux/arm64") echo "b6765d97f20cecef0121559ee26a2f0dfbac6aef49c48c71eb703271cb3f527b" ;; \ "linux/arm64") echo "8f36e45d99d2367a5c0c220ee3164fa48f4f0cec35f78204ccced8dc303bfbdc" ;; \
*) echo "" ;; esac) && \ *) echo "" ;; esac) && \
export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \ export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
"linux/amd64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/${FFMPEG_DATE}/ffmpeg-N-${FFMPEG_VERSION}-linux64-gpl.tar.xz" ;; \ "linux/amd64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/${FFMPEG_DATE}/ffmpeg-N-${FFMPEG_VERSION}-linux64-gpl.tar.xz" ;; \

View File

@@ -29,6 +29,10 @@ runcontainer:
$(docker) run --rm --name $(name) --env-file dev.env --log-opt max-size=50m -ti -p 4848:4848 $(image) $(docker) run --rm --name $(name) --env-file dev.env --log-opt max-size=50m -ti -p 4848:4848 $(image)
stopcontainer:
$(docker) stop $(name)
test: build test: build
cd tubesync && $(python) manage.py test --verbosity=2 && cd .. cd tubesync && $(python) manage.py test --verbosity=2 && cd ..

View File

@@ -24,7 +24,7 @@ $ docker exec -i tubesync python3 /app/manage.py dumpdata > some-file.json
Then change you database backend over, then use Then change you database backend over, then use
```bash ```bash
$ cat some-file.json | docker exec -i tubesync python3 /app/manage.py loaddata --format=json - $ cat some-file.json | docker exec -i tubesync python3 /app/manage.py loaddata - --format=json
``` ```
As detailed in the Django documentation: As detailed in the Django documentation:
@@ -78,3 +78,46 @@ entry in the container or stdout logs:
If you see a line similar to the above and the web interface loads, congratulations, If you see a line similar to the above and the web interface loads, congratulations,
you are now using an external database server for your TubeSync data! you are now using an external database server for your TubeSync data!
## Docker Compose
If you're using Docker Compose and simply want to connect to another container with
the DB for the performance benefits, a configuration like this would be enough:
```
tubesync-db:
image: postgres:15.2
container_name: tubesync-db
restart: unless-stopped
volumes:
- /<path/to>/init.sql:/docker-entrypoint-initdb.d/init.sql
- /<path/to>/tubesync-db:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=testpassword
tubesync:
image: ghcr.io/meeb/tubesync:latest
container_name: tubesync
restart: unless-stopped
ports:
- 4848:4848
volumes:
- /<path/to>/tubesync/config:/config
- /<path/to>/YouTube:/downloads
environment:
- DATABASE_CONNECTION=postgresql://postgres:testpassword@tubesync-db:5432/tubesync
depends_on:
- tubesync-db
```
Note that an `init.sql` file is needed to initialize the `tubesync`
database before it can be written to. This file should contain:
```
CREATE DATABASE tubesync;
```
Then it must be mapped to `/docker-entrypoint-initdb.d/init.sql` for it
to be executed on first startup of the container. See the `tubesync-db`
volume mapping above for how to do this.

View File

@@ -1,10 +1,14 @@
import logging import logging
from django.conf import settings
logging_level = logging.DEBUG if settings.DEBUG else logging.INFO
log = logging.getLogger('tubesync') log = logging.getLogger('tubesync')
log.setLevel(logging.DEBUG) log.setLevel(logging_level)
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG) ch.setLevel(logging_level)
formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s') formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s')
ch.setFormatter(formatter) ch.setFormatter(formatter)
log.addHandler(ch) log.addHandler(ch)

View File

@@ -3,7 +3,7 @@
<div class="col s12"> <div class="col s12">
<div class="pagination"> <div class="pagination">
{% for i in paginator.page_range %} {% for i in paginator.page_range %}
<a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}{% if show_skipped %}&show_skipped=yes{% endif %}">{{ i }}</a> <a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}{% if show_skipped %}&show_skipped=yes{% endif %}{% if only_skipped %}&only_skipped=yes{% endif %}">{{ i }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,17 @@
# Generated by pac
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0018_source_subtitles'),
]
operations = [
migrations.AddField(
model_name='source',
name='delete_removed_media',
field=models.BooleanField(default=False, help_text='Delete media that is no longer on this playlist', verbose_name='delete removed media'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2.22 on 2023-10-24 17:25
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0019_add_delete_removed_media'),
]
operations = [
migrations.AddField(
model_name='source',
name='filter_text',
field=models.CharField(blank=True, default='', help_text='Regex compatible filter string for video titles', max_length=100, verbose_name='filter string'),
),
migrations.AlterField(
model_name='source',
name='auto_subtitles',
field=models.BooleanField(default=False, help_text='Accept auto-generated subtitles', verbose_name='accept auto-generated subs'),
),
migrations.AlterField(
model_name='source',
name='sub_langs',
field=models.CharField(default='en', help_text='List of subtitles langs to download, comma-separated. Example: en,fr or all,-fr,-live_chat', max_length=30, validators=[django.core.validators.RegexValidator(message='Subtitle langs must be a comma-separated list of langs. example: en,fr or all,-fr,-live_chat', regex='^(\\-?[\\_\\.a-zA-Z]+,)*(\\-?[\\_\\.a-zA-Z]+){1}$')], verbose_name='subs langs'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by pac
from django.db import migrations, models
from sync.models import Source
class Migration(migrations.Migration):
dependencies = [
('sync', '0020_auto_20231024_1825'),
]
operations = [
migrations.AddField(
model_name='source',
name='lightweight_metadata',
field=models.CharField(max_length=20,
default=Source.LIGHTWEIGHT_METADATA_TYPE_RAW,
choices=Source.LIGHTWEIGHT_METADATA_TYPE_CHOICES,
help_text='Lightweight metadata',
verbose_name='lightweight metadata'),
),
]

View File

@@ -1,6 +1,7 @@
import os import os
import uuid import uuid
import json import json
import re
from xml.etree import ElementTree from xml.etree import ElementTree
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -287,6 +288,18 @@ class Source(models.Model):
help_text=_('If "delete old media" is ticked, the number of days after which ' help_text=_('If "delete old media" is ticked, the number of days after which '
'to automatically delete media') 'to automatically delete media')
) )
filter_text = models.CharField(
_('filter string'),
max_length=100,
default='',
blank=True,
help_text=_('Regex compatible filter string for video titles')
)
delete_removed_media = models.BooleanField(
_('delete removed media'),
default=False,
help_text=_('Delete media that is no longer on this playlist')
)
source_resolution = models.CharField( source_resolution = models.CharField(
_('source resolution'), _('source resolution'),
max_length=8, max_length=8,
@@ -374,6 +387,24 @@ class Source(models.Model):
] ]
) )
LIGHTWEIGHT_METADATA_TYPE_RAW = 'RAW'
LIGHTWEIGHT_METADATA_TYPE_UNNECESSARY = 'UNNECESSARY'
LIGHTWEIGHT_METADATA_TYPE_FEATHER = 'FEATHER'
LIGHTWEIGHT_METADATA_TYPES = (LIGHTWEIGHT_METADATA_TYPE_RAW, LIGHTWEIGHT_METADATA_TYPE_UNNECESSARY, LIGHTWEIGHT_METADATA_TYPE_FEATHER)
LIGHTWEIGHT_METADATA_TYPE_CHOICES = (
(LIGHTWEIGHT_METADATA_TYPE_RAW, _("(LARGE) Save raw metadata")),
(LIGHTWEIGHT_METADATA_TYPE_UNNECESSARY, _("(MEDIUM) Treeshake unnecessary metadata json keys")),
(LIGHTWEIGHT_METADATA_TYPE_FEATHER, _("(TINY) if the capacity is large, Treeshake it event if it is in use")),
)
lightweight_metadata = models.CharField(
_('lightweight metadata'),
max_length=20,
default=LIGHTWEIGHT_METADATA_TYPE_RAW,
choices=LIGHTWEIGHT_METADATA_TYPE_CHOICES,
help_text=_('Lightweight metadata')
)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -510,7 +541,8 @@ class Source(models.Model):
'mm': now.strftime('%m'), 'mm': now.strftime('%m'),
'dd': now.strftime('%d'), 'dd': now.strftime('%d'),
'source': self.slugname, 'source': self.slugname,
'source_full': self.source.name, 'source_full': self.name,
'uploader': 'Some Channel Name',
'title': 'some-media-title-name', 'title': 'some-media-title-name',
'title_full': 'Some Media Title Name', 'title_full': 'Some Media Title Name',
'key': 'SoMeUnIqUiD', 'key': 'SoMeUnIqUiD',
@@ -532,6 +564,11 @@ class Source(models.Model):
except Exception as e: except Exception as e:
return '' return ''
def is_regex_match(self, media_item_title):
if not self.filter_text:
return True
return bool(re.search(self.filter_text, media_item_title))
def index_media(self): def index_media(self):
''' '''
Index the media source returning a list of media metadata as dicts. Index the media source returning a list of media metadata as dicts.
@@ -850,7 +887,7 @@ class Media(models.Model):
def get_best_video_format(self): def get_best_video_format(self):
return get_best_video_format(self) return get_best_video_format(self)
def get_format_str(self): def get_format_str(self):
''' '''
Returns a youtube-dl compatible format string for the best matches Returns a youtube-dl compatible format string for the best matches
@@ -875,7 +912,7 @@ class Media(models.Model):
else: else:
return False return False
return False return False
def get_display_format(self, format_str): def get_display_format(self, format_str):
''' '''
Returns a tuple used in the format component of the output filename. This Returns a tuple used in the format component of the output filename. This
@@ -1166,7 +1203,7 @@ class Media(models.Model):
filename = self.filename filename = self.filename
prefix, ext = os.path.splitext(filename) prefix, ext = os.path.splitext(filename)
return f'{prefix}.nfo' return f'{prefix}.nfo'
@property @property
def nfopath(self): def nfopath(self):
return self.source.directory_path / self.nfoname return self.source.directory_path / self.nfoname
@@ -1179,7 +1216,7 @@ class Media(models.Model):
filename = self.filename filename = self.filename
prefix, ext = os.path.splitext(filename) prefix, ext = os.path.splitext(filename)
return f'{prefix}.info.json' return f'{prefix}.info.json'
@property @property
def jsonpath(self): def jsonpath(self):
return self.source.directory_path / self.jsonname return self.source.directory_path / self.jsonname

View File

@@ -96,14 +96,14 @@ def media_post_save(sender, instance, created, **kwargs):
# If the media is skipped manually, bail. # If the media is skipped manually, bail.
if instance.manual_skip: if instance.manual_skip:
return return
# Triggered after media is saved # Triggered after media is saved
cap_changed = False cap_changed = False
can_download_changed = False can_download_changed = False
# Reset the skip flag if the download cap has changed if the media has not # Reset the skip flag if the download cap has changed if the media has not
# already been downloaded # already been downloaded
if not instance.downloaded: if not instance.downloaded and instance.metadata:
max_cap_age = instance.source.download_cap_date max_cap_age = instance.source.download_cap_date
filter_text = instance.source.filter_text.strip()
published = instance.published published = instance.published
if not published: if not published:
if not instance.skip: if not instance.skip:
@@ -117,11 +117,20 @@ def media_post_save(sender, instance, created, **kwargs):
else: else:
if max_cap_age: if max_cap_age:
if published > max_cap_age and instance.skip: if published > max_cap_age and instance.skip:
# Media was published after the cap date but is set to be skipped if filter_text:
log.info(f'Media: {instance.source} / {instance} has a valid ' if instance.source.is_regex_match(instance.title):
f'publishing date, marking to be unskipped') log.info(f'Media: {instance.source} / {instance} has a valid '
instance.skip = False f'publishing date and title filter, marking to be unskipped')
cap_changed = True instance.skip = False
cap_changed = True
else:
log.debug(f'Media: {instance.source} / {instance} has a valid publishing date '
f'but failed the title filter match, already marked skipped')
else:
log.info(f'Media: {instance.source} / {instance} has a valid '
f'publishing date, marking to be unskipped')
instance.skip = False
cap_changed = True
elif published <= max_cap_age and not instance.skip: elif published <= max_cap_age and not instance.skip:
log.info(f'Media: {instance.source} / {instance} is too old for ' log.info(f'Media: {instance.source} / {instance} is too old for '
f'the download cap date, marking to be skipped') f'the download cap date, marking to be skipped')
@@ -130,10 +139,20 @@ def media_post_save(sender, instance, created, **kwargs):
else: else:
if instance.skip: if instance.skip:
# Media marked to be skipped but source download cap removed # Media marked to be skipped but source download cap removed
log.info(f'Media: {instance.source} / {instance} has a valid ' if filter_text:
f'publishing date, marking to be unskipped') if instance.source.is_regex_match(instance.title):
instance.skip = False log.info(f'Media: {instance.source} / {instance} has a valid '
cap_changed = True f'publishing date and title filter, marking to be unskipped')
instance.skip = False
cap_changed = True
else:
log.info(f'Media: {instance.source} / {instance} has a valid publishing date '
f'but failed the title filter match, already marked skipped')
else:
log.debug(f'Media: {instance.source} / {instance} has a valid publishing date and '
f'is already marked as not to be skipped')
cap_changed = False
# Recalculate the "can_download" flag, this may # Recalculate the "can_download" flag, this may
# need to change if the source specifications have been changed # need to change if the source specifications have been changed
if instance.metadata: if instance.metadata:

View File

@@ -142,6 +142,15 @@ def cleanup_old_media():
media.delete() media.delete()
def cleanup_removed_media(source, videos):
media_objects = Media.objects.filter(source=source, downloaded=True)
for item in media_objects:
matching_source_item = [video['id'] for video in videos if video['id'] == item.key]
if not matching_source_item:
log.info(f'{item.title} is no longer in source, removing')
item.delete()
@background(schedule=0) @background(schedule=0)
def index_source_task(source_id): def index_source_task(source_id):
''' '''
@@ -186,6 +195,9 @@ def index_source_task(source_id):
cleanup_completed_tasks() cleanup_completed_tasks()
# Tack on a cleanup of old media # Tack on a cleanup of old media
cleanup_old_media() cleanup_old_media()
if source.delete_removed_media:
log.info(f'Cleaning up media no longer in source {source}')
cleanup_removed_media(source, videos)
@background(schedule=0) @background(schedule=0)
@@ -219,13 +231,17 @@ def download_media_metadata(media_id):
log.error(f'Task download_media_metadata(pk={media_id}) called but no ' log.error(f'Task download_media_metadata(pk={media_id}) called but no '
f'media exists with ID: {media_id}') f'media exists with ID: {media_id}')
return return
if media.manual_skip: if media.manual_skip:
log.info(f'Task for ID: {media_id} skipped, due to task being manually skipped.') log.info(f'Task for ID: {media_id} skipped, due to task being manually skipped.')
return return
source = media.source source = media.source
metadata = media.index_metadata() metadata = media.index_metadata()
if source.lightweight_metadata == Source.LIGHTWEIGHT_METADATA_TYPE_FEATHER:
del metadata["formats"]
del metadata["thumbnails"]
del metadata["automatic_captions"]
del metadata["requested_formats"]
del metadata["heatmap"]
media.metadata = json.dumps(metadata, default=json_serial) media.metadata = json.dumps(metadata, default=json_serial)
upload_date = media.upload_date upload_date = media.upload_date
# Media must have a valid upload date # Media must have a valid upload date
@@ -242,6 +258,11 @@ def download_media_metadata(media_id):
log.warn(f'Media: {source} / {media} is older than cap age ' log.warn(f'Media: {source} / {media} is older than cap age '
f'{max_cap_age}, skipping') f'{max_cap_age}, skipping')
media.skip = True media.skip = True
# If the source has a search filter, check the video title matches the filter
if source.filter_text and not source.is_regex_match(media.title):
# Filter text not found in the media title. Accepts regex string, blank search filter results in this returning false
log.warn(f'Media: {source} / {media} does not match {source.filter_text}, skipping')
media.skip = True
# If the source has a cut-off check the upload date is within the allowed delta # If the source has a cut-off check the upload date is within the allowed delta
if source.delete_old_media and source.days_to_keep > 0: if source.delete_old_media and source.days_to_keep > 0:
if not isinstance(media.published, datetime): if not isinstance(media.published, datetime):

View File

@@ -132,6 +132,8 @@
<td><span class="hide-on-med-and-up">Can download?<br></span><strong>{% if media.can_download %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td> <td><span class="hide-on-med-and-up">Can download?<br></span><strong>{% if media.can_download %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr> </tr>
{% endif %} {% endif %}
{% if media.source.lightweight_metadata == "RAW" %}
<tr title="The available media formats"> <tr title="The available media formats">
<td class="hide-on-small-only">Available formats</td> <td class="hide-on-small-only">Available formats</td>
<td><span class="hide-on-med-and-up">Available formats<br></span> <td><span class="hide-on-med-and-up">Available formats<br></span>
@@ -155,7 +157,10 @@
Video: <strong>{% if video_format %}{{ video_format }} {% if video_exact %}(exact match){% else %}(fallback){% endif %}{% else %}no match{% endif %} Video: <strong>{% if video_format %}{{ video_format }} {% if video_exact %}(exact match){% else %}(fallback){% endif %}{% else %}no match{% endif %}
</strong></td> </strong></td>
</tr> </tr>
{% endif %}
</table> </table>
<p>{{ media.source.lightweight_metadata }}</p>
<p>{{ media.source }}</p>
</div> </div>
</div> </div>
{% if media.downloaded %} {% if media.downloaded %}

View File

@@ -64,5 +64,5 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% include 'pagination.html' with pagination=sources.paginator filter=source.pk show_skipped=show_skipped %} {% include 'pagination.html' with pagination=sources.paginator filter=source.pk show_skipped=show_skipped only_skipped=only_skipped%}
{% endblock %} {% endblock %}

View File

@@ -43,6 +43,10 @@
<td class="hide-on-small-only">Directory</td> <td class="hide-on-small-only">Directory</td>
<td><span class="hide-on-med-and-up">Directory<br></span><strong>{{ source.directory }}</strong></td> <td><span class="hide-on-med-and-up">Directory<br></span><strong>{{ source.directory }}</strong></td>
</tr> </tr>
<tr title="Filter text">
<td class="hide-on-small-only">Filter text</td>
<td><span class="hide-on-med-and-up">Filter text<br></span><strong>{{ source.filter_text }}</strong></td>
</tr>
<tr title="Media file name format to use for saving files"> <tr title="Media file name format to use for saving files">
<td class="hide-on-small-only">Media format</td> <td class="hide-on-small-only">Media format</td>
<td><span class="hide-on-med-and-up">Media format<br></span><strong>{{ source.media_format }}</strong></td> <td><span class="hide-on-med-and-up">Media format<br></span><strong>{{ source.media_format }}</strong></td>
@@ -115,6 +119,10 @@
<td class="hide-on-small-only">Write JSON?</td> <td class="hide-on-small-only">Write JSON?</td>
<td><span class="hide-on-med-and-up">Write JSON?<br></span><strong>{% if source.write_json %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td> <td><span class="hide-on-med-and-up">Write JSON?<br></span><strong>{% if source.write_json %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr> </tr>
<tr title="Delete media that is no longer on this playlist?">
<td class="hide-on-small-only">Delete removed media</td>
<td><span class="hide-on-med-and-up">Delete removed media<br></span><strong>{% if source.delete_removed_media %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
</tr>
{% if source.delete_old_media and source.days_to_keep > 0 %} {% if source.delete_old_media and source.days_to_keep > 0 %}
<tr title="Days after which your media from this source will be locally deleted"> <tr title="Days after which your media from this source will be locally deleted">
<td class="hide-on-small-only">Delete old media</td> <td class="hide-on-small-only">Delete old media</td>
@@ -178,7 +186,17 @@
<td><span class="hide-on-med-and-up">{{ _("Subs langs?") }}:</span><strong>{{source.sub_langs}}</strong></td> <td><span class="hide-on-med-and-up">{{ _("Subs langs?") }}:</span><strong>{{source.sub_langs}}</strong></td>
</tr> </tr>
{% endif %} {% endif %}
{% if source.lightweight_metadata %}
<tr title="{{ _('Are auto subs accepted?') }}">
<td class="hide-on-small-only">{{ _("Auto-generated subtitles?") }}:</td>
<td><span class="hide-on-med-and-up">{{ _("Auto-generated subtitles?") }}:</span><strong><i class="fas {% if source.auto_subtitles %}fa-check{% else %}fa-times{% endif %}"></i></strong></td>
</tr>
<tr title="{{ _('Subs langs?') }}">
<td class="hide-on-small-only">{{ _("Subs langs?") }}:</td>
<td><span class="hide-on-med-and-up">{{ _("Subs langs?") }}:</span><strong>{{source.sub_langs}}</strong></td>
</tr>
{% endif %}
</table> </table>
</div> </div>
</div> </div>

View File

@@ -175,6 +175,7 @@ class FrontEndTestCase(TestCase):
'directory': 'testdirectory', 'directory': 'testdirectory',
'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
'download_cap': 0, 'download_cap': 0,
'filter_text':'.*',
'index_schedule': 3600, 'index_schedule': 3600,
'delete_old_media': False, 'delete_old_media': False,
'days_to_keep': 14, 'days_to_keep': 14,
@@ -217,6 +218,7 @@ class FrontEndTestCase(TestCase):
'directory': 'testdirectory', 'directory': 'testdirectory',
'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
'download_cap': 0, 'download_cap': 0,
'filter_text':'.*',
'index_schedule': Source.IndexSchedule.EVERY_HOUR, 'index_schedule': Source.IndexSchedule.EVERY_HOUR,
'delete_old_media': False, 'delete_old_media': False,
'days_to_keep': 14, 'days_to_keep': 14,
@@ -247,6 +249,7 @@ class FrontEndTestCase(TestCase):
'directory': 'testdirectory', 'directory': 'testdirectory',
'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
'download_cap': 0, 'download_cap': 0,
'filter_text':'.*',
'index_schedule': Source.IndexSchedule.EVERY_2_HOURS, # changed 'index_schedule': Source.IndexSchedule.EVERY_2_HOURS, # changed
'delete_old_media': False, 'delete_old_media': False,
'days_to_keep': 14, 'days_to_keep': 14,
@@ -1468,6 +1471,29 @@ class FormatMatchingTestCase(TestCase):
self.media.get_best_video_format() self.media.get_best_video_format()
self.media.get_best_audio_format() self.media.get_best_audio_format()
def test_is_regex_match(self):
self.media.metadata = all_test_metadata['boring']
expected_matches = {
('.*'): (True),
('no fancy stuff'): (True),
('No fancy stuff'): (False),
('(?i)No fancy stuff'): (True), #set case insensitive flag
('no'): (True),
('Foo'): (False),
('^(?!.*fancy).*$'): (False),
('^(?!.*funny).*$'): (True),
('(?=.*f.*)(?=.{0,2}|.{4,})'): (True),
('f{4,}'): (False),
('^[^A-Z]*$'): (True),
('^[^a-z]*$'): (False),
('^[^\\s]*$'): (False)
}
for params, expected in expected_matches.items():
self.source.filter_text = params
expected_match_result = expected
self.assertEqual(self.source.is_regex_match(self.media.title), expected_match_result)
class TasksTestCase(TestCase): class TasksTestCase(TestCase):
def setUp(self): def setUp(self):

View File

@@ -294,12 +294,13 @@ class ValidateSourceView(FormView):
class EditSourceMixin: class EditSourceMixin:
model = Source model = Source
fields = ('source_type', 'key', 'name', 'directory', 'media_format', fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'media_format',
'index_schedule', 'download_media', 'download_cap', 'delete_old_media', 'index_schedule', 'download_media', 'download_cap', 'delete_old_media',
'days_to_keep', 'source_resolution', 'source_vcodec', 'source_acodec', 'delete_removed_media', 'days_to_keep', 'source_resolution', 'source_vcodec',
'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails', 'write_nfo', 'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_thumbnails',
'write_json', 'embed_metadata', 'embed_thumbnail', 'enable_sponsorblock', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail',
'sponsorblock_categories', 'write_subtitles', 'auto_subtitles', 'sub_langs') 'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles',
'auto_subtitles', 'sub_langs', 'lightweight_metadata')
errors = { errors = {
'invalid_media_format': _('Invalid media format, the media format contains ' 'invalid_media_format': _('Invalid media format, the media format contains '
'errors or is empty. Check the table at the end of ' 'errors or is empty. Check the table at the end of '

View File

@@ -6,7 +6,7 @@ CONFIG_BASE_DIR = BASE_DIR
DOWNLOADS_BASE_DIR = BASE_DIR DOWNLOADS_BASE_DIR = BASE_DIR
VERSION = '0.13.1' VERSION = '0.13.3'
SECRET_KEY = '' SECRET_KEY = ''
DEBUG = False DEBUG = False
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []