Skip to content

Commit

Permalink
✨ Restore open player in place
Browse files Browse the repository at this point in the history
- add configurable primary and secondary open actions, when click on
  thumbnail or bottom right icon
- 3 open action available: openModal, openTab and openEmbedded
  - openEmbedded replace the image with an embedded iframe with the
    player in it
- clean javascript code
  • Loading branch information
essembeh committed Mar 3, 2024
1 parent 01c5ac7 commit bd064bf
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 76 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ YOURSS_TTL_METADATA=60
YOURSS_TTL_RSS=60

YOURSS_CLEAN_TITLES=true
YOURSS_THEME=dark
YOURSS_THEME=dark
YOURSS_OPEN_PRIMARY=openEmbedded
YOURSS_OPEN_SECONDARY=openModal
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ I simply wrote a minimal RSS client webapp for web browsers.
- you can have *user* pages and you can configure which associated channels
- support *Redis* for caching and speeding up the webapp
- support *light* and *dark* themes
- you can choose th ebehavior when you click on the video thumbnail:
- open a modal dialog with the player (default)
- the player will replace the thumbnail
- open a new tab with the video

# Install

Expand Down Expand Up @@ -86,6 +90,8 @@ Then visit [http://localhost:8000/](http://localhost:8000/)
- `YOURSS_TTL_RSS`: the TTL of cached RSS feeds (default is `3600`, 1 hour)
- `YOURSS_CLEAN_TITLES`: if set to `true`, videos titles are cleaned to prevent UPPERCASE TITLES
- `YOURSS_THEME`: choose between `dark` and `light` Bootstrap themes (default is `light`)
- `YOURSS_OPEN_PRIMARY`: choose the action when you click on a thumbnail, can be `openEmbedded`, `openTab` or `openModal`, default is `openModal`
- `YOURSS_OPEN_SECONDARY`: choose the action when you click on the bottom right icon, can be `openEmbedded`, `openTab` or `openModal`, default is `openTab`
- `LOGURU_LEVEL` for logging level

> Note: channels can be Youtube username (like `@JonnyGiger`) or directly a *channel_id* (24 alnum chars) like `UCa_Dlwrwv3ktrhCy91HpVRw`, to provide a list, use a coma between channels
Expand Down
4 changes: 4 additions & 0 deletions charts/yourss/templates/yourss.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ spec:
value: "{{ .Values.yourss.cleanTitles }}"
- name: YOURSS_THEME
value: "{{ .Values.yourss.theme }}"
- name: YOURSS_OPEN_PRIMARY
value: "{{ .Values.yourss.openPrimary }}"
- name: YOURSS_OPEN_SECONDARY
value: "{{ .Values.yourss.openSecondary }}"
ports:
- name: http
containerPort: 8000
Expand Down
3 changes: 3 additions & 0 deletions charts/yourss/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ yourss:
logLevel: INFO
cleanTitles: true
theme: light
# open actions can be: openModal, openTab or openEmbedded
openPrimary: openModal
openSecondary: openTab
defaultChannels:
- "@jonnygiger"
users:
Expand Down
2 changes: 2 additions & 0 deletions yourss/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class AppConfig:
TTL_RSS = environ.var(converter=int, default=3600)
CLEAN_TITLES = environ.bool_var(default=False)
THEME = environ.var("light")
OPEN_PRIMARY = environ.var("openModal")
OPEN_SECONDARY = environ.var("openTab")


YOURSS_USER_PREFIX = "YOURSS_USER_"
Expand Down
141 changes: 88 additions & 53 deletions yourss/static/yourss.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Scroll to top
*/
$(window).scroll(function () {
var height = $(window).scrollTop()
let height = $(window).scrollTop()
if (height > 100) {
$("#scroll-to-top").fadeIn()
} else {
Expand Down Expand Up @@ -31,37 +31,37 @@ var SORT_ORDERS = [
//"bi bi-sort-alpha-up",
]
function toggle_sort(button) {
old_order = $(button).find("i").attr("class")
new_order =
SORT_ORDERS[(SORT_ORDERS.indexOf(old_order) + 1) % SORT_ORDERS.length]
$(button).find("i").attr("class", new_order)
sort_videos(new_order)
let oldOrder = $(button).find("i").attr("class")
let newOrder =
SORT_ORDERS[(SORT_ORDERS.indexOf(oldOrder) + 1) % SORT_ORDERS.length]
$(button).find("i").attr("class", newOrder)
sort_videos(newOrder)
}
function sort_videos(order) {
$("#video-container").html(
$("#video-container")
.children()
.sort(function (a, b) {
a_channel = $(a).data("channel-title")
a_date = new Date($(a).data("published"))
b_channel = $(b).data("channel-title")
b_date = new Date($(b).data("published"))
.sort(function (left, right) {
let leftChannel = $(left).data("channel-title")
let leftDate = new Date($(left).data("published"))
let rightChannel = $(right).data("channel-title")
let rightDate = new Date($(right).data("published"))
if (order === "bi bi-sort-down") {
if (a_date < b_date) return 1
if (a_date > b_date) return -1
if (leftDate < rightDate) return 1
if (leftDate > rightDate) return -1
} else if (order === "bi bi-sort-up") {
if (a_date < b_date) return -1
if (a_date > b_date) return 1
if (leftDate < rightDate) return -1
if (leftDate > rightDate) return 1
} else if (order === "bi bi-sort-alpha-down") {
if (a_channel < b_channel) return -1
if (a_channel > b_channel) return 1
if (a_date < b_date) return 1
if (a_date > b_date) return -1
if (leftChannel < rightChannel) return -1
if (leftChannel > rightChannel) return 1
if (leftDate < rightDate) return 1
if (leftDate > rightDate) return -1
} else if (order === "bi bi-sort-alpha-up") {
if (a_channel < b_channel) return -1
if (a_channel > b_channel) return 1
if (a_date < b_date) return -1
if (a_date > b_date) return 1
if (leftChannel < rightChannel) return -1
if (leftChannel > rightChannel) return 1
if (leftDate < rightDate) return -1
if (leftDate > rightDate) return 1
}
return 0
})
Expand All @@ -75,11 +75,11 @@ $(document).ready(function () {
* Handle filter toggle
*/
function toggle_filter(button) {
selected_channel_id = $(button).hasClass("btn-secondary")
let selectedChannelId = $(button).hasClass("btn-secondary")
? $(button).data("channel-id")
: null
$(".yourss-filter").each(function () {
if ($(this).data("channel-id") === selected_channel_id) {
if ($(this).data("channel-id") === selectedChannelId) {
$(this).addClass("btn-primary")
$(this).removeClass("btn-secondary")
} else {
Expand All @@ -88,9 +88,9 @@ function toggle_filter(button) {
}
})
$(".yourss-filterable").each(function () {
if (selected_channel_id === null) {
if (selectedChannelId === null) {
$(this).css("display", "block")
} else if ($(this).data("channel-id") === selected_channel_id) {
} else if ($(this).data("channel-id") === selectedChannelId) {
$(this).css("display", "block")
} else {
$(this).css("display", "none")
Expand All @@ -103,16 +103,16 @@ function toggle_filter(button) {
*/
function mark_as_read(date) {
if (date) {
mark_date = new Date(date)
let markDate = new Date(date)
$(".yourss-filterable").each(function () {
video_date = new Date($(this).data("published"))
video_mark = $(this).find(".new-item")
if (video_date > mark_date) {
video_mark.addClass("bi-record-fill")
video_mark.removeClass("bi-record")
let videoDate = new Date($(this).data("published"))
let videoMark = $(this).find(".new-item")
if (videoDate > markDate) {
videoMark.addClass("bi-record-fill")
videoMark.removeClass("bi-record")
} else {
video_mark.addClass("bi-record")
video_mark.removeClass("bi-record-fill")
videoMark.addClass("bi-record")
videoMark.removeClass("bi-record-fill")
}
})
Cookies.set("mark-date:" + window.location.pathname, date)
Expand All @@ -125,31 +125,66 @@ $(document).ready(function () {
/**
* Handle modal player
*/
function play_video(id) {
channel_id = $(id).data("channel-id")
channel_title = $(id).data("channel-title")
video_id = $(id).data("video-id")
video_title = $(id).data("video-title")
function closeAllPlayers() {
$(".yourss-player-container").each(function () {
$(this).children().remove()
$(this).css("display", "none")
})
$(".yourss-thumbnail").each(function () {
$(this).css("display", "block")
})
}

function getVideoPlayerUrl(videoId) {
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&control=2&rel=0`
}

function openEmbedded(videoId) {
closeAllPlayers()

let height = $(`#yourss-thumbnail-${videoId}`).height()
let videoUrl = getVideoPlayerUrl(videoId)

$(`#yourss-thumbnail-${videoId}`).css("display", "none")
$(`#yourss-player-${videoId}`).css("display", "block")
$(`#yourss-player-${videoId}`).append(`
<iframe src="${videoUrl}"
width="100%" height="${height}"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen=""frameborder="0"></iframe>`)
}

function openTab(videoId) {
closeAllPlayers()

let videoUrl = getVideoPlayerUrl(videoId)
window.open(videoUrl, "_blank")
}

function openModal(videoId) {
closeAllPlayers()

let videoDiv = $(`#yourss-video-${videoId}`)
let channelId = videoDiv.data("channel-id")
let channelTitle = videoDiv.data("channel-title")
let videoTitle = videoDiv.data("video-title")
let videoUrl = getVideoPlayerUrl(videoId)

$("#yourss-modal").data("video-id", video_id)
$("#yourss-modal")
.find("iframe")
.attr(
"src",
`https://www.youtube-nocookie.com/embed/${video_id}?autoplay=1&control=2&rel=0`
)
$("#yourss-modal-channel-image").attr("src", `/api/avatar/${channel_id}`)
$("#yourss-modal-channel-title").text(channel_title)
$("#yourss-modal-video-title").text(video_title)
$("#yourss-modal").data("video-id", videoId)
$("#yourss-modal").find("iframe").attr("src", videoUrl)
$("#yourss-modal-channel-image").attr("src", `/api/avatar/${channelId}`)
$("#yourss-modal-channel-title").text(channelTitle)
$("#yourss-modal-video-title").text(videoTitle)
$("#yourss-modal-link-youtube").attr(
"href",
`https://www.youtube.com/watch?v=${video_id}`
`https://www.youtube.com/watch?v=${videoId}`
)
$("#yourss-modal-link-tab").attr("href", videoUrl)
$("#yourss-modal-link-piped").attr(
"href",
`https://piped.kavin.rocks/watch?v=${video_id}`
`https://piped.kavin.rocks/watch?v=${videoId}`
)
$("#yourss-modal-link-rss").attr("href", `/api/rss/${channel_id}`)
$("#yourss-modal-link-rss").attr("href", `/api/rss/${channelId}`)
$("#yourss-modal").modal("show")
}
$("#yourss-modal").on("hidden.bs.modal", function (e) {
Expand Down
8 changes: 6 additions & 2 deletions yourss/templates/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
{% for video in feed.entries %}
<div class="col-xxl-2 col-xl-3 col-lg-3 col-md-4 col-sm-6 mb-3 yourss-filterable" id="yourss-video-{{ video.video_id }}" data-video-id="{{ video.video_id }}" data-channel-id="{{ feed.channel_id }}" data-channel-title="{{ feed.title|escape }}" data-video-title="{{ video.title|escape }}" data-published="{{ video.published }}">
<div class="card h-100 shadow">
<img class="card-img-top" src="{{ video.thumbnail_url }}" style="cursor: pointer" alt="{{ video.title|escape }}" onclick="play_video('#yourss-video-{{ video.video_id }}')" />
<img id="yourss-thumbnail-{{ video.video_id }}" class="card-img-top yourss-thumbnail" src="{{ video.thumbnail_url }}" onclick="{{ open_primary }}('{{ video.video_id }}')" style="cursor: pointer" alt="{{ video.title|escape }}" />
<div class="yourss-player-container" id="yourss-player-{{ video.video_id }}" style="display: none;"></div>
<div class="card-body d-flex flex-column">
<h5 class="card-title h5">
<img src="/api/avatar/{{ feed.channel_id }}" width="30" height="30" class="rounded-circle" />
Expand All @@ -74,7 +75,7 @@ <h5 class="card-title h5">
<i class="new-item bi bi-record-fill text-primary" onclick="mark_as_read('{{ video.published }}')">
<small class="text-muted" title="{{ video.published }}">{{ video.published_moment }}</small>
</i>
<a class="bi bi-box-arrow-up-right" style="float: right; color:black;" href="/watch?v={{ video.video_id }}" target="_blank"></a>
<a class="bi bi-box-arrow-up-right" style="float: right;" onclick="{{ open_secondary }}('{{ video.video_id }}')" target="_blank"></a>
</p>
</div>
</div>
Expand Down Expand Up @@ -109,6 +110,9 @@ <h5 class="card-title h5">
<a id="yourss-modal-link-youtube" class="btn btn-danger" target="_blank" onclick="$('#yourss-modal').modal('hide')">
<i class="bi bi-youtube"></i>
</a>
<a id="yourss-modal-link-tab" class="btn btn-primary" target="_blank" onclick="$('#yourss-modal').modal('hide')">
<i class="bi bi-fullscreen"></i>
</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-lg"></i>
</button>
Expand Down
15 changes: 15 additions & 0 deletions yourss/utils.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
from typing import Callable

from starlette.templating import Jinja2Templates, _TemplateResponse


def parse_channel_names(text: str, delimiter: str = ",") -> set[str]:
return set(filter(None, text.split(delimiter)))


def custom_template_response(
jinja: Jinja2Templates, template_file: str, **kwargs
) -> Callable[..., _TemplateResponse]:
def func(**kwargs2) -> _TemplateResponse:
kwargs2.update(kwargs)
return jinja.TemplateResponse(template_file, kwargs2)

return func
38 changes: 18 additions & 20 deletions yourss/webapp/www.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import yourss

from ..config import YOURSS_USERS, current_config
from ..utils import parse_channel_names
from ..utils import custom_template_response, parse_channel_names
from ..youtube import YoutubeWebClient
from .utils import get_youtube_client

Expand All @@ -31,7 +31,13 @@ class Theme(str, Enum):
# Jinja customization
env = Environment(loader=FileSystemLoader(Path(yourss.__file__).parent / "templates"))
env.filters["clean_title"] = clean_title
TemplateResponse = Jinja2Templates(env=env).TemplateResponse
ViewTemplateResponse = custom_template_response(
Jinja2Templates(env=env),
"view.html",
version=yourss.__version__,
open_primary=current_config.OPEN_PRIMARY,
open_secondary=current_config.OPEN_SECONDARY,
)

router = APIRouter()

Expand Down Expand Up @@ -70,15 +76,11 @@ async def get_user(
if len(feeds) == 0:
raise HTTPException(status_code=404, detail="No channels found")

return TemplateResponse(
"view.html",
{
"request": request,
"title": f"/u/{user}",
"feeds": feeds,
"theme": theme.value if theme is not None else current_config.THEME,
"version": yourss.__version__,
},
return ViewTemplateResponse(
request=request,
title=f"/u/{user}",
feeds=feeds,
theme=theme.value if theme is not None else current_config.THEME,
)


Expand All @@ -99,13 +101,9 @@ async def view_channels(
if len(feeds) == 0:
raise HTTPException(status_code=404, detail="No channels found")

return TemplateResponse(
"view.html",
{
"request": request,
"title": ", ".join(sorted(map(lambda f: f.title, feeds))),
"feeds": feeds,
"theme": theme.value if theme is not None else current_config.THEME,
"version": yourss.__version__,
},
return ViewTemplateResponse(
request=request,
title=", ".join(sorted(map(lambda f: f.title, feeds))),
feeds=feeds,
theme=theme.value if theme is not None else current_config.THEME,
)

0 comments on commit bd064bf

Please sign in to comment.