This commit is contained in:
Greg 2024-02-05 14:42:10 -07:00 committed by GitHub
commit b3cf72210e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 270 additions and 32 deletions

View File

@ -25,9 +25,24 @@ server {
}
}
location / {
#allow access to resources via url token
location ~ ^/auth/(\w+)/(.*)$ {
set $token $1;
set $url $2;
proxy_pass $scheme://$http_host/$url;
proxy_set_header Authorization "Token $token";
proxy_pass_request_headers on;
}
#protect podcast url via header authorization token
location ~ ^/channel/([\w-_]+)/podcast/$ {
auth_request /api/ping/;
include uwsgi_params;
uwsgi_pass localhost:8080;
}
location / {
include uwsgi_params;
uwsgi_pass localhost:8080;
}
}

View File

@ -36,6 +36,11 @@ urlpatterns = [
views.VideoSponsorView.as_view(),
name="api-video-sponsor",
),
path(
"video/<slug:video_id>/mp3/",
views.VideoMP3View.as_view(),
name="api-video-mp3",
),
path(
"channel/",
views.ChannelApiListView.as_view(),

View File

@ -1,5 +1,7 @@
"""all API views"""
import os
from api.src.aggs import (
BiggestChannel,
Channel,
@ -10,6 +12,8 @@ from api.src.aggs import (
WatchProgress,
)
from api.src.search_processor import SearchProcess
from django.core.files.temp import NamedTemporaryFile
from django.http import FileResponse
from home.src.download.queue import PendingInteract
from home.src.download.subscriptions import (
ChannelSubscription,
@ -23,6 +27,7 @@ from home.src.frontend.searching import SearchForm
from home.src.frontend.watched import WatchState
from home.src.index.channel import YoutubeChannel
from home.src.index.generic import Pagination
from home.src.index.manual import ImportFolderScanner
from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.index.video import SponsorBlock, YoutubeVideo
@ -310,6 +315,28 @@ class VideoSponsorView(ApiBaseView):
return response, status_code
class VideoMP3View(ApiBaseView):
"""resolves to /api/video/<video_id>/mp3/
handle mp3 version of video
"""
def get(self, request, video_id):
video = YoutubeVideo(video_id)
video.get_from_es()
video_filepath = os.path.join(
EnvironmentSettings.MEDIA_DIR,
video.json_data["channel"]["channel_id"],
video.json_data["youtube_id"] + ".mp4",
)
audio_filepath = NamedTemporaryFile(suffix=".mp3")
ImportFolderScanner.convert_media(video_filepath, audio_filepath.name)
return FileResponse(
open(audio_filepath.name, "rb"),
as_attachment=True,
filename=video.json_data["youtube_id"] + ".mp3",
)
class ChannelApiView(ApiBaseView):
"""resolves to /api/channel/<channel_id>/
GET: returns metadata dict of channel

View File

@ -298,13 +298,25 @@ class YoutubeChannel(YouTubeItem):
return all_youtube_ids
def get_channel_videos(self):
def get_channel_videos(self, detail_level=1):
"""get all videos from channel"""
source = ["youtube_id", "vid_type"]
if detail_level == 2:
source = [
"youtube_id",
"vid_type",
"title",
"media_size",
"description",
"media_url",
"vid_last_refresh",
"player",
]
data = {
"query": {
"term": {"channel.channel_id": {"value": self.youtube_id}}
},
"_source": ["youtube_id", "vid_type"],
"_source": source,
}
all_videos = IndexPaginate("ta_video", data).get_results()
return all_videos

View File

@ -359,6 +359,20 @@ class ImportFolderScanner:
return new_path
@staticmethod
def convert_media(input_path, output_path):
"""convert media file format"""
subprocess.run(
[
"ffmpeg",
"-y",
"-i",
input_path,
output_path,
],
check=True,
)
def _convert_video(self, current_video):
"""convert if needed"""
current_path = current_video["media"]

View File

@ -91,14 +91,16 @@ def requests_headers() -> dict[str, str]:
return {"User-Agent": template}
def date_praser(timestamp: int | str) -> str:
def date_praser(timestamp: int | str, format: str = None) -> str:
"""return formatted date string"""
if isinstance(timestamp, int):
date_obj = datetime.fromtimestamp(timestamp)
elif isinstance(timestamp, str):
date_obj = datetime.strptime(timestamp, "%Y-%m-%d")
return date_obj.date().isoformat()
if format is None:
return date_obj.date().isoformat()
else:
return date_obj.strftime(format)
def time_parser(timestamp: str) -> float:

View File

@ -28,27 +28,36 @@
</div>
<div id="notifications" data="channel reindex"></div>
<div class="info-box info-box-2">
<div class="info-box-item">
<div class="round-img">
<a href="{% url 'channel_id' channel_info.channel_id %}">
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
</a>
</div>
<div>
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
{% if channel_info.channel_subs >= 1000000 %}
<p>Subscribers: {{ channel_info.channel_subs|intword }}</p>
{% else %}
<p>Subscribers: {{ channel_info.channel_subs|intcomma }}</p>
{% endif %}
{% if channel_info.channel_subscribed %}
{% if request.user|has_group:"admin" or request.user.is_staff %}
<button class="unsubscribe" type="button" data-type="channel" data-subscribe="" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ channel_info.channel_name }}">Unsubscribe</button>
{% endif %}
{% else %}
<button type="button" data-type="channel" data-subscribe="true" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ channel_info.channel_name }}">Subscribe</button>
{% endif %}
</div>
<div class="info-box-item two">
<div class="info-box-item">
<div class="round-img">
<a href="{% url 'channel_id' channel_info.channel_id %}">
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
</a>
</div>
<div>
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
{% if channel_info.channel_subs >= 1000000 %}
<p>Subscribers: {{ channel_info.channel_subs|intword }}</p>
{% else %}
<p>Subscribers: {{ channel_info.channel_subs|intcomma }}</p>
{% endif %}
{% if channel_info.channel_subscribed %}
{% if request.user|has_group:"admin" or request.user.is_staff %}
<button class="unsubscribe" type="button" data-type="channel" data-subscribe="" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ channel_info.channel_name }}">Unsubscribe</button>
{% endif %}
{% else %}
<button type="button" data-type="channel" data-subscribe="true" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ channel_info.channel_name }}">Subscribe</button>
{% endif %}
</div>
</div>
<div class="rss-icons">
<img id="rss-audio-button" src="{% static 'img/icon-rss.svg' %}" alt="rss-icon" data-context="video" onclick="showForm('rss_buttons')" title="Podcast">
</div>
<div id="rss_buttons" class="hidden button-box">
<button type="button" onclick="copyToClipboard('{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}{% url 'channel_id_podcast' channel_info.channel_id 'audio' %}')">Copy Audio Podcast to clipboard</button>
<button type="button" onclick="copyToClipboard('{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}{% url 'channel_id_podcast' channel_info.channel_id 'video' %}')">Copy Video Podcast to clipboard</button>
</div>
</div>
<div class="info-box-item">
{% if aggs %}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
xmlns:podcast="https://podcastindex.org/namespace/1.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>{{ channel_info.channel_name|slice:":255" }}</title>
<link>{{ request.scheme }}://{{ request.META.HTTP_HOST }}{% url 'channel_id' channel_info.channel_id %}</link>
<description>{{ channel_info.channel_description|slice:":4000" }}</description>
<language>en-us</language>
<itunes:category text="Music"/>
<itunes:explicit>false</itunes:explicit>
<podcast:locked>yes</podcast:locked>
<itunes:image href="{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" />
<atom:link href="{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}{% url 'channel_id_podcast' channel_info.channel_id format %}" rel="self" type="application/rss+xml"/>{% if results %}{% for video in results %}
<item>
<title>{{ video.title|slice:":255" }}</title>
<link>{{ request.scheme }}://{{ request.META.HTTP_HOST }}{% url 'video' video.youtube_id %}</link>
<description>{{ video.description|slice:":4000" }}</description>
<pubDate>{{ video.vid_last_refresh }}</pubDate>
<guid isPermaLink="false">{{ video.youtube_id }}</guid>
<enclosure length="{{ video.media_size }}" type="{{ mime_format }}" url="{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}{{ video.media_url }}"/>
<itunes:duration>{{ video.player.duration }}</itunes:duration>
<itunes:image href="{{ request.scheme }}://{{ request.META.HTTP_HOST }}/auth/{{ api_token }}/cache/{{ video.vid_thumb_path }}"/>
<itunes:block>yes</itunes:block>
</item>{% endfor %}{% endif %}
</channel>
</rss>

View File

@ -88,6 +88,11 @@ urlpatterns = [
login_required(views.ChannelIdPlaylistView.as_view()),
name="channel_id_playlist",
),
path(
"channel/<slug:channel_id>/podcast/<slug:format>/",
views.ChannelIdPodcastView.as_view(),
name="channel_id_podcast",
),
path(
"video/<slug:video_id>/",
login_required(views.VideoView.as_view()),

View File

@ -19,6 +19,7 @@ from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
from home.src.download.queue import PendingInteract
from home.src.download.thumbnails import ThumbManager
from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.backup import ElasticBackup
from home.src.es.connect import ElasticWrap
@ -34,13 +35,13 @@ from home.src.frontend.forms import (
SubscribeToPlaylistForm,
UserSettingsForm,
)
from home.src.index.channel import channel_overwrites
from home.src.index.channel import YoutubeChannel, channel_overwrites
from home.src.index.generic import Pagination
from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.helper import check_stylesheet, time_parser
from home.src.ta.helper import check_stylesheet, date_praser, time_parser
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.users import UserConfig
@ -524,6 +525,7 @@ class ChannelIdView(ChannelIdBaseView):
{
"title": "Channel: " + channel_name,
"channel_info": channel_info,
"api_token": SettingsApplicationView.get_token(request),
}
)
@ -659,6 +661,49 @@ class ChannelIdPlaylistView(ChannelIdBaseView):
self.data["query"] = {"bool": {"must": must_list}}
class ChannelIdPodcastView(ChannelIdBaseView):
"""resolves to /channel/<channel-id>/podcast/<format>/
display single channel podcast from channel_id
"""
def get(self, request, channel_id, format):
"""handle get request"""
channel = YoutubeChannel(channel_id)
channel.get_from_es()
channel_info = channel.json_data
results = channel.get_channel_videos(2)
is_audio = format == "audio"
mime_format = "audio/mp3" if is_audio else "video/mp4"
for video in results:
video["vid_last_refresh"] = date_praser(
video["vid_last_refresh"], "%a, %d %b %Y %H:%M:%S -0000"
)
video["vid_thumb_path"] = ThumbManager(
video["youtube_id"]
).vid_thumb_path()
if is_audio:
video["media_url"] = (
"/api/video/" + video["youtube_id"] + "/mp3/"
)
else:
video["media_url"] = "/media/" + video["media_url"]
self.context = {
"channel_info": channel_info,
"results": results,
"api_token": request.headers["Authorization"][6:]
if "Authorization" in request.headers
else "none",
"format": format,
"mime_format": mime_format,
}
return render(
request,
"home/channel_id_podcast.xml",
self.context,
content_type="application/rss+xml",
)
class ChannelView(ArchivistResultsView):
"""resolves to /channel/
handle functionality for channel overview page, subscribe to channel,

View File

@ -120,6 +120,10 @@ button:hover {
color: var(--main-bg);
}
#rss_buttons button {
margin-bottom: 5px;
}
.button-box {
padding: 5px 0;
}
@ -350,6 +354,7 @@ button:hover {
margin: 15px 0;
}
.rss-icons,
.view-icons,
.grid-count {
display: flex;
@ -371,7 +376,7 @@ button:hover {
filter: var(--img-filter);
}
#hidden-form {
#hidden-form, .hidden {
display: none;
}
@ -670,6 +675,13 @@ video:-webkit-full-screen {
margin: 0 15px;
}
.rss-icons > img {
width: 30px;
margin: 5px 10px;
cursor: pointer;
filter: var(--img-filter);
}
.info-box-item {
display: flex;
flex-wrap: wrap;
@ -678,6 +690,10 @@ video:-webkit-full-screen {
background-color: var(--highlight-bg);
}
.info-box-item.two {
justify-content: space-between;
}
.info-box-item p {
width: 100%;
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 461.432 461.432" xml:space="preserve">
<g id="lines__x003C_Group_x003E_">
<g id="circle">
<path d="M125.896,398.928c0,33.683-27.308,60.999-61.022,60.999c-33.684,0-61.006-27.316-61.006-60.999
c0-33.729,27.322-61.038,61.006-61.038C98.588,337.89,125.896,365.198,125.896,398.928z"/>
</g>
<g>
<g>
<path d="M0,229.636c0,8.441,6.606,15.379,15.036,15.809c60.318,3.076,100.885,25.031,138.248,62.582
c36.716,36.864,60.071,89.759,64.082,137.769c0.686,8.202,7.539,14.524,15.77,14.524h56.701c4.344,0,8.498-1.784,11.488-4.935
c2.992-3.15,4.555-7.391,4.333-11.729c-8.074-158.152-130.669-278.332-289.013-286.23c-4.334-0.217-8.564,1.355-11.709,4.344
C1.792,164.759,0,168.908,0,173.247V229.636z"/>
</g>
</g>
<g>
<g>
<path d="M0,73.411c0,8.51,6.713,15.482,15.216,15.819c194.21,7.683,350.315,161.798,358.098,355.879
c0.34,8.491,7.32,15.208,15.818,15.208h56.457c4.297,0,8.408-1.744,11.393-4.834c2.985-3.09,4.585-7.258,4.441-11.552
C453.181,199.412,261.024,9.27,16.38,1.121C12.089,0.978,7.925,2.583,4.838,5.568C1.751,8.551,0,12.661,0,16.954V73.411z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1383,8 +1383,10 @@ function textExpandButtonVisibilityUpdate() {
document.addEventListener('readystatechange', textExpandButtonVisibilityUpdate);
window.addEventListener('resize', textExpandButtonVisibilityUpdate);
function showForm() {
let formElement = document.getElementById('hidden-form');
function showForm(id) {
let id2 = id === undefined ? 'hidden-form' : id;
let formElement = document.getElementById(id2);
let displayStyle = formElement.style.display;
if (displayStyle === '') {
formElement.style.display = 'block';
@ -1567,3 +1569,34 @@ function doShortcut(e) {
}
}
}
function fallbackCopyToClipboard(text) {
var textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
}
catch (err) {
console.error('fallbackCopyToClipboard: Unable to copy', err);
}
document.body.removeChild(textArea);
}
function copyToClipboard(text) {
if (!navigator.clipboard) {
fallbackCopyToClipboard(text);
return;
}
navigator.clipboard.writeText(text)
}