Compare commits

..

5 commits

Author SHA1 Message Date
7c741a476f Bump version: 0.2.0 → 0.3.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-27 23:09:56 +02:00
e5c1fbdfb4 add search 2022-06-27 23:09:42 +02:00
df90e42729 allow variable number of feed items 2022-06-27 22:44:16 +02:00
f21387a23c add opml download 2022-06-27 22:38:12 +02:00
ded1895adb limit downloads view to 100 items 2022-06-27 21:56:18 +02:00
18 changed files with 202 additions and 55 deletions

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.2.0 current_version = 0.3.0
commit = True commit = True
tag = True tag = True

View file

@ -19,16 +19,16 @@
grid-column: auto grid-column: auto
@include tablet @include tablet
grid-template-columns: repeat(3,minmax(0,1fr)) grid-template-columns: repeat(3, minmax(0, 1fr))
@include desktop @include desktop
grid-template-columns: repeat(4,minmax(0,1fr)) grid-template-columns: repeat(4, minmax(0, 1fr))
@include widescreen @include widescreen
grid-template-columns: repeat(5,minmax(0,1fr)) grid-template-columns: repeat(5, minmax(0, 1fr))
@include fullhd @include fullhd
grid-template-columns: repeat(6,minmax(0,1fr)) grid-template-columns: repeat(6, minmax(0, 1fr))
.video-card .video-card
display: flex display: flex
@ -43,3 +43,6 @@
// Fix almost invisible navbar items on mobile // Fix almost invisible navbar items on mobile
.navbar-item .navbar-item
color: #fff color: #fff
.overflow-x
overflow-x: auto

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "ucast" name = "ucast"
version = "0.2.0" version = "0.3.0"
description = "YouTube to Podcast converter" description = "YouTube to Podcast converter"
authors = ["Theta-Dev <t.testboy@gmail.com>"] authors = ["Theta-Dev <t.testboy@gmail.com>"]
packages = [ packages = [

View file

@ -1,4 +1,4 @@
__version__ = "0.2.0" __version__ = "0.3.0"
def template_context(request): def template_context(request):

View file

@ -147,6 +147,12 @@ class UcastFeed(Feed):
return Channel.objects.get(slug=channel_slug) return Channel.objects.get(slug=channel_slug)
def get_feed(self, channel: Channel, request: http.HttpRequest): def get_feed(self, channel: Channel, request: http.HttpRequest):
max_items = settings.FEED_MAX_ITEMS
try:
max_items = int(request.GET.get("items"))
except TypeError or ValueError:
pass
feed = self.feed_type( feed = self.feed_type(
title=channel.name, title=channel.name,
link=channel.get_absolute_url(), link=channel.get_absolute_url(),
@ -158,7 +164,7 @@ class UcastFeed(Feed):
for video in channel.video_set.filter(downloaded__isnull=False).order_by( for video in channel.video_set.filter(downloaded__isnull=False).order_by(
"-published" "-published"
)[: settings.FEED_MAX_ITEMS]: )[:max_items]:
feed.add_item( feed.add_item(
title=video.title, title=video.title,
link=video.get_absolute_url(), link=video.get_absolute_url(),

View file

@ -92,11 +92,11 @@ def get_failed_job_registry():
return registry.FailedJobRegistry(queue.name, queue.connection) return registry.FailedJobRegistry(queue.name, queue.connection)
def get_downloading_videos(): def get_downloading_videos(offset=0, limit=-1):
queue = get_queue() queue = get_queue()
videos = {} videos = {}
for job in queue.jobs: for job in queue.get_jobs(offset, limit):
if ( if (
job.func_name == "ucast.tasks.download.download_video" job.func_name == "ucast.tasks.download.download_video"
and job.args and job.args

40
ucast/service/opml.py Normal file
View file

@ -0,0 +1,40 @@
from dataclasses import dataclass
from typing import Iterable
from django.utils.xmlutils import SimplerXMLGenerator
from ucast.models import Channel
@dataclass
class FeedElement:
url: str
title: str
def __add_feed_element(handler: SimplerXMLGenerator, element: FeedElement):
handler.addQuickElement(
"outline", attrs={"xmlUrl": element.url, "title": element.title}
)
def write_opml(elements: Iterable[FeedElement], outfile):
handler = SimplerXMLGenerator(outfile, "utf-8", short_empty_elements=True)
handler.startDocument()
handler.startElement("opml", {})
handler.addQuickElement("head")
handler.startElement("body", {"version": "1.0"})
for element in elements:
__add_feed_element(handler, element)
handler.endElement("body")
handler.endElement("opml")
handler.endDocument()
def write_channels_opml(channels: Iterable[Channel], site_url: str, key: str, outfile):
elements = [
FeedElement(f"{site_url}/feed/{c.slug}?key={key}", c.name) for c in channels
]
write_opml(elements, outfile)

View file

@ -10363,4 +10363,8 @@ a.has-text-danger-dark:hover, a.has-text-danger-dark:focus {
color: #fff; color: #fff;
} }
.overflow-x {
overflow-x: auto;
}
/*# sourceMappingURL=style.css.map */ /*# sourceMappingURL=style.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -27,6 +27,9 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<a class="navbar-item" href="{% url 'search' %}">
Search
</a>
<a class="navbar-item" href="{% url 'downloads' %}"> <a class="navbar-item" href="{% url 'downloads' %}">
Downloads Downloads
</a> </a>

View file

@ -68,6 +68,10 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div>
<a href="{% url 'channels_opml' %}">Download OPML</a>
</div>
{% endblock content %} {% endblock content %}
{% block javascript %} {% block javascript %}

View file

@ -1,36 +1,23 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}ucast - Errors{% endblock %} {% block title %}ucast - Downloads{% endblock %}
{% block content %} {% block content %}
<div class="mb-4"> <div class="mb-4">
<div> <div>
<span class="title">Downloading <span class="title">Downloading</span>
{% if downloading_videos %}({{ downloading_videos.paginator.count }}){% endif %}
</span>
</div> </div>
</div> </div>
<div class="mb-4"> {% if downloading_videos %}
{% if downloading_videos %} <div class="mb-4" hx-get="{% url 'downloads' %}" hx-trigger="every 5s">
<table class="table">
<thead>
<tr>
<th>Video-ID</th>
<th>Title</th>
<th>Channel</th>
<th>Published</th>
</tr>
</thead>
<tbody hx-get="?items" hx-trigger="every 5s">
{% include "ucast/downloads_items.html" %} {% include "ucast/downloads_items.html" %}
{% else %} </div>
{% else %}
<div class="mb-4">
<p>Not downloading any videos</p> <p>Not downloading any videos</p>
{% endif %} </div>
</tbody> {% endif %}
</table>
</div>
<div class="mb-4"> <div class="mb-4">
<div> <div>

View file

@ -1,13 +1,26 @@
{% for video in downloading_videos %} <div class="mb-4">
<tr {% if forloop.last and downloading_videos.has_next %} <a class="subtitle">{{ n_downloading_videos }} Videos</a>
hx-get="?page={{ downloading_videos.next_page_number }}" </div>
hx-trigger="revealed" <div class="mb-4 overflow-x">
hx-swap="afterend" <table class="table">
{% endif %}> <thead>
<td><a href="{{ video.get_absolute_url }}">{{ video.video_id }}</a></td> <tr>
<td>{{ video.title }}</td> <th>Video-ID</th>
<td><a href="{% url 'videos' video.channel.slug %}">{{ video.channel.name }}</a> <th>Title</th>
</td> <th>Channel</th>
<td>{{ video.published|date:"SHORT_DATE_FORMAT" }}</td> <th>Published</th>
</tr> </tr>
{% endfor %} </thead>
<tbody>
{% for video in downloading_videos %}
<tr>
<td><a href="{{ video.get_absolute_url }}">{{ video.video_id }}</a></td>
<td>{{ video.title }}</td>
<td><a href="{% url 'videos' video.channel.slug %}">{{ video.channel.name }}</a>
</td>
<td>{{ video.published|date:"SHORT_DATE_FORMAT" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View file

@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% block title %}ucast - Search{% endblock %}
{% block content %}
<div class="box">
<form method="get">
<div class="field has-addons">
<div class="control is-flex-grow-1">
<input name="q" required class="input" type="text"
placeholder="Search" {% if query %}value="{{ query }}{% endif %}">
</div>
<div class="control">
<button type="submit" class="button is-primary">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="video-grid">
{% if videos %}
{% for video in videos %}
<div class="card video-card">
<a href="{{ video.get_absolute_url }}" target="_blank">
<img class="video-thumbnail"
src="/files/thumbnail/{{ video.channel.slug }}/{{ video.slug }}.webp?sm">
</a>
<div class="video-card-content is-flex-grow-1">
<a href="{{ video.get_absolute_url }}">{{ video.title }}</a>
</div>
<div class="video-card-content">
<div class="level">
<span class="tag">
<i
class="fas fa-calendar"></i>&nbsp; {{ video.published|date:"SHORT_DATE_FORMAT" }}
</span>
<div class="field has-addons">
<div class="control">
<a class="button is-small is-success"
href="/files/audio/{{ video.channel.slug }}/{{ video.slug }}.mp3"
target="_blank">
<i class="fas fa-play"></i>
</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% elif query %}
<p>No videos</p>
{% endif %}
</div>
{% endblock content %}

View file

@ -7,7 +7,7 @@
{% endif %}> {% endif %}>
<a href="{{ video.get_absolute_url }}" target="_blank"> <a href="{{ video.get_absolute_url }}" target="_blank">
<img class="video-thumbnail" <img class="video-thumbnail"
src="/files/thumbnail/{{ channel.slug }}/{{ video.slug }}.webp?sm"> src="/files/thumbnail/{{ video.channel.slug }}/{{ video.slug }}.webp?sm">
</a> </a>
<div class="video-card-content is-flex-grow-1"> <div class="video-card-content is-flex-grow-1">
@ -25,7 +25,7 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<a class="button is-small is-success" <a class="button is-small is-success"
href="/files/audio/{{ channel.slug }}/{{ video.slug }}.mp3" href="/files/audio/{{ video.channel.slug }}/{{ video.slug }}.mp3"
target="_blank"> target="_blank">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</a> </a>

View file

@ -30,6 +30,8 @@ urlpatterns = [
), ),
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("search", views.search, name="search"),
path("files/audio/<str:channel>/<str:video>", views.audio), path("files/audio/<str:channel>/<str:video>", views.audio),
path("files/cover/<str:channel>/<str:video>", views.cover), path("files/cover/<str:channel>/<str:video>", views.cover),
path("files/thumbnail/<str:channel>/<str:video>", views.thumbnail), path("files/thumbnail/<str:channel>/<str:video>", views.thumbnail),

View file

@ -16,7 +16,7 @@ from django.utils.decorators import decorator_from_middleware
from ucast import feed, forms, queue from ucast import feed, forms, queue
from ucast.models import Channel, User, Video from ucast.models import Channel, User, Video
from ucast.service import controller, storage from ucast.service import controller, opml, storage
from ucast.tasks import download from ucast.tasks import download
@ -159,12 +159,10 @@ def downloads(request: http.HttpRequest):
ids = freg.get_job_ids(0, 50) ids = freg.get_job_ids(0, 50)
failed_jobs = freg.job_class.fetch_many(ids, freg.connection, freg.serializer) failed_jobs = freg.job_class.fetch_many(ids, freg.connection, freg.serializer)
page_number = request.GET.get("page") downloading_videos = queue.get_downloading_videos(limit=100)
downloading_videos = queue.get_downloading_videos()
downloading_videos_p = Paginator(downloading_videos, 100)
template_name = "ucast/downloads.html" template_name = "ucast/downloads.html"
if request.htmx or request.GET.get("items"): if request.htmx:
template_name = "ucast/downloads_items.html" template_name = "ucast/downloads_items.html"
return render( return render(
@ -172,7 +170,8 @@ def downloads(request: http.HttpRequest):
template_name, template_name,
{ {
"failed_jobs": failed_jobs, "failed_jobs": failed_jobs,
"downloading_videos": downloading_videos_p.get_page(page_number), "downloading_videos": downloading_videos,
"n_downloading_videos": 1,
}, },
) )
@ -205,6 +204,31 @@ def download_errors_requeue_all(request: http.HttpRequest):
return http.HttpResponseRedirect(reverse(downloads)) return http.HttpResponseRedirect(reverse(downloads))
@login_required
def channels_opml(request: http.HttpRequest):
response = http.HttpResponse(
content_type="application/xml",
headers={"Content-Disposition": "attachment; filename=ucast_channels.opml"},
)
site_url = add_domain(get_current_site(request).domain, "", request.is_secure())
opml.write_channels_opml(
Channel.objects.all(), site_url, request.user.get_feed_key(), response
)
return response
@login_required
def search(request: http.HttpRequest):
query = request.GET.get("q")
vids = []
if query:
vids = Video.objects.filter(downloaded__isnull=False, title__icontains=query)[
:30
]
return render(request, "ucast/search.html", {"query": query, "videos": vids})
def _channel_file(channel: str, get_file: Callable[[storage.ChannelFolder], Path]): def _channel_file(channel: str, get_file: Callable[[storage.ChannelFolder], Path]):
store = storage.Storage() store = storage.Storage()