Compare commits
3 commits
fff882894f
...
3479365c52
Author | SHA1 | Date | |
---|---|---|---|
3479365c52 | |||
9d53d79f95 | |||
83e1d9a406 |
10 changed files with 214 additions and 45 deletions
21
.drone.yml
21
.drone.yml
|
@ -36,6 +36,27 @@ steps:
|
||||||
depends_on:
|
depends_on:
|
||||||
- install dependencies
|
- install dependencies
|
||||||
|
|
||||||
|
- name: build container
|
||||||
|
image: quay.io/buildah/stable
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
commands:
|
||||||
|
- buildah login -u $DOCKER_USER -p $DOCKER_PASS -- $DOCKER_REGISTRY
|
||||||
|
- buildah manifest create ucast
|
||||||
|
- buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch amd64 -f deploy/Dockerfile .
|
||||||
|
- buildah bud --tag code.thetadev.de/hsa/ucast:latest --manifest ucast --arch arm64 -f deploy/Dockerfile .
|
||||||
|
- buildah manifest push --all ucast docker://code.thetadev.de/hsa/ucast:latest
|
||||||
|
environment:
|
||||||
|
DOCKER_REGISTRY:
|
||||||
|
from_secret: docker_registry
|
||||||
|
DOCKER_USER:
|
||||||
|
from_secret: docker_username
|
||||||
|
DOCKER_PASS:
|
||||||
|
from_secret: docker_password
|
||||||
|
depends_on:
|
||||||
|
- test
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
temp: { }
|
temp: { }
|
||||||
|
|
|
@ -3,6 +3,7 @@ import os
|
||||||
|
|
||||||
from django.db.models import ObjectDoesNotExist
|
from django.db.models import ObjectDoesNotExist
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from yt_dlp.utils import DownloadError
|
||||||
|
|
||||||
from ucast import queue
|
from ucast import queue
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
|
@ -21,7 +22,25 @@ def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
||||||
try:
|
try:
|
||||||
video = Video.objects.get(video_id=vid.id)
|
video = Video.objects.get(video_id=vid.id)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
details = youtube.get_video_details(vid.id)
|
try:
|
||||||
|
details = youtube.get_video_details(vid.id)
|
||||||
|
except DownloadError as e:
|
||||||
|
if "available" in e.msg:
|
||||||
|
# Create dummy video to prevent further download attempts
|
||||||
|
# of unavailable videos
|
||||||
|
video = Video(
|
||||||
|
video_id=vid.id,
|
||||||
|
title="",
|
||||||
|
slug="",
|
||||||
|
channel=channel,
|
||||||
|
published=timezone.datetime(2000, 1, 1, tzinfo=timezone.utc),
|
||||||
|
description="",
|
||||||
|
duration=0,
|
||||||
|
is_deleted=True,
|
||||||
|
)
|
||||||
|
video.save()
|
||||||
|
return
|
||||||
|
raise e
|
||||||
|
|
||||||
# Dont load active livestreams
|
# Dont load active livestreams
|
||||||
if details.is_currently_live:
|
if details.is_currently_live:
|
||||||
|
@ -50,20 +69,23 @@ def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
||||||
and video.is_deleted is False
|
and video.is_deleted is False
|
||||||
and channel.should_download(video)
|
and channel.should_download(video)
|
||||||
):
|
):
|
||||||
queue.enqueue(download_video, video)
|
queue.enqueue(download_video, video.id)
|
||||||
|
|
||||||
redis.delete(lock_key)
|
redis.delete(lock_key)
|
||||||
|
|
||||||
|
|
||||||
def download_video(video: Video):
|
def download_video(v_id: int):
|
||||||
"""
|
"""
|
||||||
Download a video including its thumbnail, create a cover image
|
Download a video including its thumbnail, create a cover image
|
||||||
and store everything in the channel folder.
|
and store everything in the channel folder.
|
||||||
|
|
||||||
:param video: Video object
|
:param v_id: Video ID
|
||||||
"""
|
"""
|
||||||
# Return if the video was already downloaded by a previous task
|
# Return if the video was already downloaded by a previous task
|
||||||
video.refresh_from_db()
|
try:
|
||||||
|
video = Video.objects.get(id=v_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
if video.downloaded:
|
if video.downloaded:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -71,7 +93,14 @@ def download_video(video: Video):
|
||||||
channel_folder = store.get_or_create_channel_folder(video.channel.slug)
|
channel_folder = store.get_or_create_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
audio_file = channel_folder.get_audio(video.slug)
|
audio_file = channel_folder.get_audio(video.slug)
|
||||||
details = youtube.download_audio(video.video_id, audio_file)
|
try:
|
||||||
|
details = youtube.download_audio(video.video_id, audio_file)
|
||||||
|
except DownloadError as e:
|
||||||
|
if "available" in e.msg:
|
||||||
|
video.is_deleted = True
|
||||||
|
video.save()
|
||||||
|
return
|
||||||
|
raise e
|
||||||
|
|
||||||
# Download/convert thumbnails
|
# Download/convert thumbnails
|
||||||
tn_path = channel_folder.get_thumbnail(video.slug)
|
tn_path = channel_folder.get_thumbnail(video.slug)
|
||||||
|
@ -106,8 +135,12 @@ def download_video(video: Video):
|
||||||
video.save()
|
video.save()
|
||||||
|
|
||||||
|
|
||||||
def update_channel(channel: Channel):
|
def update_channel(c_id: int):
|
||||||
"""Update a single channel from its RSS feed"""
|
"""Update a single channel from its RSS feed"""
|
||||||
|
try:
|
||||||
|
channel = Channel.objects.get(id=c_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
videos = youtube.get_channel_videos_from_feed(channel.channel_id)
|
videos = youtube.get_channel_videos_from_feed(channel.channel_id)
|
||||||
|
|
||||||
for vid in videos:
|
for vid in videos:
|
||||||
|
@ -123,18 +156,23 @@ def update_channels():
|
||||||
This task is scheduled a regular intervals.
|
This task is scheduled a regular intervals.
|
||||||
"""
|
"""
|
||||||
for channel in Channel.objects.filter(active=True):
|
for channel in Channel.objects.filter(active=True):
|
||||||
queue.enqueue(update_channel, channel)
|
queue.enqueue(update_channel, channel.id)
|
||||||
|
|
||||||
|
|
||||||
def download_channel(channel: Channel, limit: int):
|
def download_channel(c_id: int, limit: int):
|
||||||
"""
|
"""
|
||||||
Download maximum number of videos from a channel.
|
Download maximum number of videos from a channel.
|
||||||
|
|
||||||
:param channel: Channel object
|
:param c_id: Channel ID (Database)
|
||||||
:param limit: Max number of videos
|
:param limit: Max number of videos
|
||||||
"""
|
"""
|
||||||
if limit < 1:
|
if limit < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel = Channel.objects.get(id=c_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
for vid in youtube.get_channel_videos_from_scraper(channel.channel_id, limit):
|
for vid in youtube.get_channel_videos_from_scraper(channel.channel_id, limit):
|
||||||
_load_scraped_video(vid, channel)
|
_load_scraped_video(vid, channel)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.db.models import ObjectDoesNotExist
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from ucast import queue
|
from ucast import queue
|
||||||
|
@ -7,7 +8,12 @@ from ucast.models import Channel, Video
|
||||||
from ucast.service import cover, storage, util, videoutil, youtube
|
from ucast.service import cover, storage, util, videoutil, youtube
|
||||||
|
|
||||||
|
|
||||||
def recreate_cover(video: Video):
|
def recreate_cover(v_id: int):
|
||||||
|
try:
|
||||||
|
video = Video.objects.get(id=v_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
cf = store.get_channel_folder(video.channel.slug)
|
cf = store.get_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
|
@ -42,7 +48,7 @@ def recreate_cover(video: Video):
|
||||||
|
|
||||||
def recreate_covers():
|
def recreate_covers():
|
||||||
for video in Video.objects.filter(downloaded__isnull=False):
|
for video in Video.objects.filter(downloaded__isnull=False):
|
||||||
queue.enqueue(recreate_cover, video)
|
queue.enqueue(recreate_cover, video.id)
|
||||||
|
|
||||||
|
|
||||||
def update_file_storage():
|
def update_file_storage():
|
||||||
|
@ -81,7 +87,12 @@ def update_file_storage():
|
||||||
video.save()
|
video.save()
|
||||||
|
|
||||||
|
|
||||||
def update_channel_info(channel: Channel):
|
def update_channel_info(ch_id: int):
|
||||||
|
try:
|
||||||
|
channel = Channel.objects.get(id=ch_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
channel_data = youtube.get_channel_metadata(
|
channel_data = youtube.get_channel_metadata(
|
||||||
youtube.channel_url_from_id(channel.channel_id)
|
youtube.channel_url_from_id(channel.channel_id)
|
||||||
)
|
)
|
||||||
|
@ -104,4 +115,4 @@ def update_channel_info(channel: Channel):
|
||||||
|
|
||||||
def update_channel_infos():
|
def update_channel_infos():
|
||||||
for channel in Channel.objects.filter(active=True):
|
for channel in Channel.objects.filter(active=True):
|
||||||
queue.enqueue(update_channel_info, channel)
|
queue.enqueue(update_channel_info, channel.id)
|
||||||
|
|
|
@ -27,11 +27,15 @@
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
{% if failed_jobs %}
|
{% if failed_jobs %}
|
||||||
<div class="mb-4">
|
<div class="level mb-4">
|
||||||
<form method="post" action="{% url 'download_errors_requeue_all' %}">
|
<form method="post" action="{% url 'download_errors_requeue_all' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-primary">Requeue all</button>
|
<button class="button is-primary">Requeue all</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" action="{% url 'download_errors_delete_all' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-danger">Delete all</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
@ -41,6 +45,7 @@
|
||||||
<th>Function</th>
|
<th>Function</th>
|
||||||
<th>Details</th>
|
<th>Details</th>
|
||||||
<th>Requeue</th>
|
<th>Requeue</th>
|
||||||
|
<th>Delete</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -56,6 +61,13 @@
|
||||||
<button class="button is-small">Requeue</button>
|
<button class="button is-small">Requeue</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{% url 'download_errors_delete' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ job.id }}">
|
||||||
|
<button class="button is-small is-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -16,12 +16,17 @@
|
||||||
{{ job.exc_info }}
|
{{ job.exc_info }}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<div>
|
<div class="level">
|
||||||
<form method="post" action="{% url 'download_errors_requeue' %}">
|
<form method="post" action="{% url 'download_errors_requeue' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="id" value="{{ job.id }}">
|
<input type="hidden" name="id" value="{{ job.id }}">
|
||||||
<button class="button is-primary">Requeue</button>
|
<button class="button is-primary">Requeue</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" action="{% url 'download_errors_delete' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ job.id }}">
|
||||||
|
<button class="button is-danger">Delete</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-user-group"></i> {{ channel.subscribers }}</span>
|
class="fas fa-user-group"></i> {{ channel.subscribers }}</span>
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-video"></i> {{ videos.paginator.count }}</span>
|
class="fas fa-video"></i> {{ videos.paginator.count }}
|
||||||
|
{% if n_pending %}
|
||||||
|
({{ n_pending }})
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-database"></i> {{ channel.download_size|filesizeformat }}</span>
|
class="fas fa-database"></i> {{ channel.download_size|filesizeformat }}</span>
|
||||||
<a class="tag" href="{{ channel.get_absolute_url }}" target="_blank"><i
|
<a class="tag" href="{{ channel.get_absolute_url }}" target="_blank"><i
|
||||||
|
@ -44,13 +48,14 @@
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="button is-info" href="{% url 'channel_download' channel.slug %}">
|
<a class="button is-info" href="{% url 'channel_download' channel.slug %}">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" name="delete_channel" class="button is-danger dialog-confirm"
|
<button type="submit" name="delete_channel"
|
||||||
|
class="button is-danger dialog-confirm"
|
||||||
confirm-msg="Do you want to delete the channel '{{ channel.name }}' including {{ videos|length }} videos?">
|
confirm-msg="Do you want to delete the channel '{{ channel.name }}' including {{ videos|length }} videos?">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -60,28 +65,38 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-grid">
|
{% if not videos %}
|
||||||
{% if not videos %}
|
{% if n_pending %}
|
||||||
<p>No videos</p>
|
<p>There are {{ n_pending }} videos waiting to be downloaded.
|
||||||
|
Please wait a few minutes and refesh this page.
|
||||||
|
You can see the current status in the <i>Downloads</i> tab.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>No videos. If you have just added this channel,
|
||||||
|
you have to wait a minute for ucast to start looking for videos.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "ucast/videos_items.html" %}
|
{% else %}
|
||||||
</div>
|
<div class="video-grid">
|
||||||
|
{% include "ucast/videos_items.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if videos.has_previous or videos.has_next %}
|
{% if videos.has_previous or videos.has_next %}
|
||||||
<noscript>
|
<noscript>
|
||||||
<nav class="pagination is-centered mt-4" role="navigation" aria-label="pagination">
|
<nav class="pagination is-centered mt-4" role="navigation"
|
||||||
{% if videos.has_previous %}
|
aria-label="pagination">
|
||||||
<a class="pagination-previous" href="?page={{ videos.previous_page_number }}">Previous</a>
|
{% if videos.has_previous %}
|
||||||
{% else %}
|
<a class="pagination-previous" href="?page={{ videos.previous_page_number }}">Previous</a>
|
||||||
<a class="pagination-previous" disabled>Previous</a>
|
{% else %}
|
||||||
{% endif %}
|
<a class="pagination-previous" disabled>Previous</a>
|
||||||
{% if videos.has_next %}
|
{% endif %}
|
||||||
<a class="pagination-next" href="?page={{ videos.next_page_number }}">Next
|
{% if videos.has_next %}
|
||||||
page</a>
|
<a class="pagination-next" href="?page={{ videos.next_page_number }}">Next
|
||||||
{% else %}
|
page</a>
|
||||||
<a class="pagination-previous" disabled>Previous</a>
|
{% else %}
|
||||||
{% endif %}
|
<a class="pagination-previous" disabled>Previous</a>
|
||||||
</nav>
|
{% endif %}
|
||||||
</noscript>
|
</nav>
|
||||||
|
</noscript>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from ucast import queue, tests
|
from ucast import queue, tests
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
from ucast.service import storage
|
from ucast.service import storage
|
||||||
|
from ucast.service.youtube import VideoScraped
|
||||||
from ucast.tasks import download
|
from ucast.tasks import download
|
||||||
|
|
||||||
CHANNEL_ID_THETADEV = "UCGiJh0NZ52wRhYKYnuZI08Q"
|
CHANNEL_ID_THETADEV = "UCGiJh0NZ52wRhYKYnuZI08Q"
|
||||||
VIDEO_ID_INTRO = "I0RRENheeTo"
|
VIDEO_ID_INTRO = "I0RRENheeTo"
|
||||||
VIDEO_SLUG_INTRO = "20211010_No_copyright_intro_free_fire_intro_no_text_free_copy_right_free_templates_free_download"
|
VIDEO_SLUG_INTRO = "20211010_No_copyright_intro_free_fire_intro_no_text_free_copy_right_free_templates_free_download"
|
||||||
|
VIDEO_ID_UNAVAILABLE = "K6CBuTy09CE"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_download_video(download_dir, rq_queue):
|
def test_download_video(download_dir, rq_queue):
|
||||||
video = Video.objects.get(video_id=VIDEO_ID_INTRO)
|
video = Video.objects.get(video_id=VIDEO_ID_INTRO)
|
||||||
job = queue.enqueue(download.download_video, video)
|
job = queue.enqueue(download.download_video, video.id)
|
||||||
|
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
cf = store.get_or_create_channel_folder(video.channel.slug)
|
cf = store.get_or_create_channel_folder(video.channel.slug)
|
||||||
|
@ -28,6 +31,35 @@ def test_download_video(download_dir, rq_queue):
|
||||||
assert os.path.isfile(cf.get_thumbnail(VIDEO_SLUG_INTRO, True))
|
assert os.path.isfile(cf.get_thumbnail(VIDEO_SLUG_INTRO, True))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_load_unavailable_video(download_dir, rq_queue, mock_redis):
|
||||||
|
channel = Channel.objects.get(channel_id=CHANNEL_ID_THETADEV)
|
||||||
|
download._load_scraped_video(VideoScraped(VIDEO_ID_UNAVAILABLE, None), channel)
|
||||||
|
|
||||||
|
video = Video.objects.get(video_id=VIDEO_ID_UNAVAILABLE)
|
||||||
|
assert video.is_deleted is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_download_unavailable_video(download_dir, rq_queue):
|
||||||
|
channel = Channel.objects.get(channel_id=CHANNEL_ID_THETADEV)
|
||||||
|
video = Video(
|
||||||
|
video_id=VIDEO_ID_UNAVAILABLE,
|
||||||
|
title="",
|
||||||
|
slug="",
|
||||||
|
channel=channel,
|
||||||
|
published=timezone.datetime(2000, 1, 1, tzinfo=timezone.utc),
|
||||||
|
description="",
|
||||||
|
duration=0,
|
||||||
|
)
|
||||||
|
video.save()
|
||||||
|
job = queue.enqueue(download.download_video, video.id)
|
||||||
|
video.refresh_from_db()
|
||||||
|
|
||||||
|
assert job.is_finished
|
||||||
|
assert video.is_deleted
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_update_channel(
|
def test_update_channel(
|
||||||
download_dir, rq_queue, mock_redis, mock_get_video_details, mock_download_audio
|
download_dir, rq_queue, mock_redis, mock_get_video_details, mock_download_audio
|
||||||
|
@ -37,7 +69,7 @@ def test_update_channel(
|
||||||
Video.objects.get(video_id="_I5IFObm_-k").delete()
|
Video.objects.get(video_id="_I5IFObm_-k").delete()
|
||||||
|
|
||||||
channel = Channel.objects.get(channel_id=CHANNEL_ID_THETADEV)
|
channel = Channel.objects.get(channel_id=CHANNEL_ID_THETADEV)
|
||||||
job = rq_queue.enqueue(download.update_channel, channel)
|
job = rq_queue.enqueue(download.update_channel, channel.id)
|
||||||
assert job.is_finished
|
assert job.is_finished
|
||||||
|
|
||||||
mock_download_audio.assert_any_call(
|
mock_download_audio.assert_any_call(
|
||||||
|
|
|
@ -19,7 +19,7 @@ def test_recreate_cover(download_dir_content_mut, rq_queue, mocker):
|
||||||
store = storage.Storage()
|
store = storage.Storage()
|
||||||
cf = store.get_or_create_channel_folder(video.channel.slug)
|
cf = store.get_or_create_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
job = rq_queue.enqueue(library.recreate_cover, video)
|
job = rq_queue.enqueue(library.recreate_cover, video.id)
|
||||||
assert job.is_finished
|
assert job.is_finished
|
||||||
|
|
||||||
create_cover_mock.assert_called_once_with(
|
create_cover_mock.assert_called_once_with(
|
||||||
|
@ -53,7 +53,7 @@ def test_update_channel_info(rq_queue, mock_get_channel_metadata):
|
||||||
channel.avatar_url = "Old avatar url"
|
channel.avatar_url = "Old avatar url"
|
||||||
channel.save()
|
channel.save()
|
||||||
|
|
||||||
job = rq_queue.enqueue(library.update_channel_info, channel)
|
job = rq_queue.enqueue(library.update_channel_info, channel.id)
|
||||||
assert job.is_finished
|
assert job.is_finished
|
||||||
|
|
||||||
channel.refresh_from_db()
|
channel.refresh_from_db()
|
||||||
|
|
|
@ -28,6 +28,16 @@ urlpatterns = [
|
||||||
views.download_errors_requeue_all,
|
views.download_errors_requeue_all,
|
||||||
name="download_errors_requeue_all",
|
name="download_errors_requeue_all",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"downloads/delete",
|
||||||
|
views.download_errors_delete,
|
||||||
|
name="download_errors_delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"downloads/delete_all",
|
||||||
|
views.download_errors_delete_all,
|
||||||
|
name="download_errors_delete_all",
|
||||||
|
),
|
||||||
path("downloads/error/<str:job_id>", views.error_details, name="error_details"),
|
path("downloads/error/<str:job_id>", views.error_details, name="error_details"),
|
||||||
path("feed/<str:channel>", views.podcast_feed, name="feed"),
|
path("feed/<str:channel>", views.podcast_feed, name="feed"),
|
||||||
path("opml", views.channels_opml, name="channels_opml"),
|
path("opml", views.channels_opml, name="channels_opml"),
|
||||||
|
|
|
@ -33,7 +33,7 @@ def home(request: http.HttpRequest):
|
||||||
channel_str = form.cleaned_data["channel_str"]
|
channel_str = form.cleaned_data["channel_str"]
|
||||||
try:
|
try:
|
||||||
channel = controller.create_channel(channel_str)
|
channel = controller.create_channel(channel_str)
|
||||||
queue.enqueue(download.update_channel, channel)
|
queue.enqueue(download.update_channel, channel.id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
form.add_error("channel_str", "Channel URL invalid")
|
form.add_error("channel_str", "Channel URL invalid")
|
||||||
except controller.ChannelAlreadyExistsException:
|
except controller.ChannelAlreadyExistsException:
|
||||||
|
@ -91,6 +91,10 @@ def videos(request: http.HttpRequest, channel: str):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
template_name = "ucast/videos_items.html"
|
template_name = "ucast/videos_items.html"
|
||||||
|
|
||||||
|
n_pending = Video.objects.filter(
|
||||||
|
channel=chan, downloaded__isnull=True, is_deleted=False
|
||||||
|
).count()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
template_name,
|
template_name,
|
||||||
|
@ -98,6 +102,7 @@ def videos(request: http.HttpRequest, channel: str):
|
||||||
"videos": videos_p.get_page(page_number),
|
"videos": videos_p.get_page(page_number),
|
||||||
"channel": chan,
|
"channel": chan,
|
||||||
"site_url": site_url,
|
"site_url": site_url,
|
||||||
|
"n_pending": n_pending,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -139,7 +144,7 @@ def channel_download(request: http.HttpRequest, channel: str):
|
||||||
form = forms.DownloadChannelForm(request.POST)
|
form = forms.DownloadChannelForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
queue.enqueue(
|
queue.enqueue(
|
||||||
download.download_channel, chan, form.cleaned_data["n_videos"]
|
download.download_channel, chan.id, form.cleaned_data["n_videos"]
|
||||||
)
|
)
|
||||||
return http.HttpResponseRedirect(reverse(videos, args=[channel]))
|
return http.HttpResponseRedirect(reverse(videos, args=[channel]))
|
||||||
|
|
||||||
|
@ -204,6 +209,26 @@ def download_errors_requeue_all(request: http.HttpRequest):
|
||||||
return http.HttpResponseRedirect(reverse(downloads))
|
return http.HttpResponseRedirect(reverse(downloads))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def download_errors_delete(request: http.HttpRequest):
|
||||||
|
form = forms.RequeueForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
freg.remove(str(form.cleaned_data["id"]), delete_job=True)
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(downloads))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def download_errors_delete_all(request: http.HttpRequest):
|
||||||
|
freg = queue.get_failed_job_registry()
|
||||||
|
for job_id in freg.get_job_ids():
|
||||||
|
freg.remove(job_id, delete_job=True)
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(downloads))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def channels_opml(request: http.HttpRequest):
|
def channels_opml(request: http.HttpRequest):
|
||||||
response = http.HttpResponse(
|
response = http.HttpResponse(
|
||||||
|
|
Loading…
Reference in a new issue