Merge 1870601342
into 8778546577
This commit is contained in:
commit
b3cf72210e
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue