diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 49b8a86..872348b 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.4.5") implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + implementation("com.mohamedrejeb.ksoup:ksoup-html:0.1.4") + testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") } diff --git a/backend/src/main/kotlin/de/randomerror/ytsync/Favicon.kt b/backend/src/main/kotlin/de/randomerror/ytsync/Favicon.kt new file mode 100644 index 0000000..3ed4bf1 --- /dev/null +++ b/backend/src/main/kotlin/de/randomerror/ytsync/Favicon.kt @@ -0,0 +1,134 @@ +package de.randomerror.ytsync + +import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler +import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser +import java.net.MalformedURLException +import java.net.URL +import kotlin.time.Duration.Companion.seconds + +private val FAVICON_CACHE = mutableMapOf() + +fun getInitialFavicon(query: String, youtubeId: String?): String? { + val isYoutube = youtubeId != null || !query.matches(Regex("^(ftp|https?)://.*")) + val url = if (isYoutube) { + URL("https://www.youtube.com/") + } else { + try { + URL(query) + } catch (e: MalformedURLException) { + return null + } + } + + return if (url.authority in FAVICON_CACHE) { + FAVICON_CACHE[url.authority] + } else { + url.toURI().resolve("/favicon.ico").toString() + } +} +fun getFavicon(query: String, videoUrl: String): String? { + val url = try { + URL(query) + } catch (e: MalformedURLException) { + URL(videoUrl) + } + if (url.authority in FAVICON_CACHE) { + return FAVICON_CACHE[url.authority] + } + val favicon = fetchFavicon(url) + FAVICON_CACHE[url.authority] = favicon + return favicon +} + +private fun fetchFavicon(url: URL): String { + try { + val icons = mutableListOf>() + val handler = KsoupHtmlHandler.Builder() + .onOpenTag { name, attributes, _ -> + val href = attributes["href"] + val rel = attributes["rel"] + if (name == "link" && (rel == "icon" || rel == "shortcut icon") && href != null) { + val icon = url.toURI().resolve(href).toString() + val type = attributes["type"] + val sizesSpec = attributes["sizes"] + val sizes = if (type != null && type.startsWith("image/svg") || sizesSpec == "any" && !href.endsWith(".ico")) { + FaviconSizes.Any + } else if (sizesSpec == null || sizesSpec == "any" && href.endsWith(".ico")) { + FaviconSizes.Unknown + } else { + val sizes = sizesSpec.split(' ') + .onEach { println("found size: $it") } + .map { it.split('x', ignoreCase = true)[0] } + .map { it.toInt() } + FaviconSizes.Sizes(sizes) + } + icons.add(icon to sizes) + } + } + .build() + val conn = url.openConnection() + conn.connectTimeout = 5.seconds.inWholeMilliseconds.toInt() + conn.readTimeout = 5.seconds.inWholeMilliseconds.toInt() + val bytes = conn.getInputStream().use { it.readNBytes(1024 * 64) } + val parser = KsoupHtmlParser(handler) + parser.write(String(bytes)) + parser.end() + val favicon = icons.maxBy { it.second }.first + FAVICON_CACHE[url.authority] = favicon + return favicon + } catch (e: Exception) { + return url.toURI().resolve("/favicon.ico").toString() + } +} + +private const val IDEAL_FAVICON_SIZE = 16 + +sealed class FaviconSizes : Comparable { + object Unknown : FaviconSizes() { + override fun compareTo(other: FaviconSizes): Int { + return -1 + } + } + + object Any : FaviconSizes() { + override fun compareTo(other: FaviconSizes): Int { + return 1 + } + } + + class Sizes(val sizes: List) : FaviconSizes() { + override fun compareTo(other: FaviconSizes): Int { + if (other !is Sizes) { + return -other.compareTo(this) + } + val best = bestSize() + val otherBest = other.bestSize() + return compareSize(best, otherBest) + } + + fun bestSize(): Int { + return sizes.maxWith(this::compareSize) + } + + private fun compareSize(sizeA: Int, sizeB: Int): Int { + return if (sizeA == IDEAL_FAVICON_SIZE && sizeB != IDEAL_FAVICON_SIZE) { + 1 + } else if (sizeA != IDEAL_FAVICON_SIZE && sizeB == IDEAL_FAVICON_SIZE) { + -1 + } else if (sizeA < IDEAL_FAVICON_SIZE) { + if (sizeB > IDEAL_FAVICON_SIZE) { + -1 + } else { + sizeA.compareTo(sizeB) + } + } else { + if (sizeB < IDEAL_FAVICON_SIZE) { + 1 + } else { + sizeB.compareTo(sizeA) + } + } + } + } +} + diff --git a/backend/src/main/kotlin/de/randomerror/ytsync/Model.kt b/backend/src/main/kotlin/de/randomerror/ytsync/Model.kt index c19b4e3..84007e1 100644 --- a/backend/src/main/kotlin/de/randomerror/ytsync/Model.kt +++ b/backend/src/main/kotlin/de/randomerror/ytsync/Model.kt @@ -44,11 +44,13 @@ data class VideoSource( ) data class QueueItem( - val source: VideoSource, + val source: VideoSource?, val originalQuery: String, val title: String?, val thumbnail: String?, - val id: String = UUID.nameUUIDFromBytes(source.url.toByteArray()).toString(), + val favicon: String?, + val loading: Boolean, + val id: String = UUID.randomUUID().toString(), ) data class User( diff --git a/backend/src/main/kotlin/de/randomerror/ytsync/Queue.kt b/backend/src/main/kotlin/de/randomerror/ytsync/Queue.kt index ca53037..a922917 100644 --- a/backend/src/main/kotlin/de/randomerror/ytsync/Queue.kt +++ b/backend/src/main/kotlin/de/randomerror/ytsync/Queue.kt @@ -24,41 +24,65 @@ fun enqueue(session: Session, query: String): String { if (youtubeId != null) { synchronized(room.queue) { if (room.queue.size == 0) { + // this is the first video, it does not go into the queue, we don't need any video info val fallbackVideo = getFallbackYoutubeVideo(query, youtubeId) - // this is the first video it does not go into the queue, we don't need any video info room.queue.add(fallbackVideo) room.broadcastAll(session, "video ${gson.toJson(fallbackVideo.source)}") - return "queue" + return "queue immediate" } } } + if (room.queue.any { it.originalQuery == query }) { + session.remote.sendStringByFuture("queue err duplicate") + return "queue err duplicate" + } + val loadingQueueItem = getInitialVideoInfo(query, youtubeId) + room.broadcastAll(session, "queue add ${gson.toJson(loadingQueueItem)}") videoInfoFetcher.execute { // try to get video info, but if it fails, use the fallback info so that the video at least plays - val video = fetchVideoInfo(query, youtubeId) ?: youtubeId?.let { getFallbackYoutubeVideo(query, it) } - if (video == null) { + val videoInfo = (fetchVideoInfo(query, youtubeId) ?: youtubeId?.let { getFallbackYoutubeVideo(query, it) }) + if (videoInfo == null) { log(session, "queue err not-found") + room.broadcastAll(session, "queue rm ${loadingQueueItem.id}") session.remote.sendStringByFuture("queue err not-found") return@execute } + val queueItem = + videoInfo.copy(id = loadingQueueItem.id, favicon = videoInfo.favicon ?: loadingQueueItem.favicon) synchronized(room.queue) { - if (room.queue.any { it.source.url == video.source.url || it.originalQuery == query }) { + if (room.queue.any { it.source != null && it.source.url == queueItem.source?.url }) { session.remote.sendStringByFuture("queue err duplicate") + room.broadcastAll(session, "queue rm ${loadingQueueItem.id}") return@execute } - room.queue.add(video) + room.queue.add(queueItem) if (room.queue.size == 1) { - room.broadcastAll(session, "video ${gson.toJson(video.source)}") + room.broadcastAll(session, "queue rm ${queueItem.id}") + room.broadcastAll(session, "video ${gson.toJson(queueItem.source)}") + return@execute } else { - room.broadcastAll(session, "queue add ${gson.toJson(video)}") + room.broadcastAll(session, "queue add ${gson.toJson(queueItem)}") + } + } + val favicon = getFavicon(query, queueItem.source!!.url) + println(favicon) + if (favicon != null && favicon != queueItem.favicon) { + synchronized(room.queue) { + val index = room.queue.indexOfFirst { it.id == queueItem.id } + if (index > 0) { + val queueItemWithFavicon = queueItem.copy(favicon = favicon) + room.broadcastAll(session, "queue add ${gson.toJson(queueItemWithFavicon)}") + room.queue[index] = queueItemWithFavicon + } } } } - return "queue" + return "queue fetching" } fun dequeue(session: Session, queueId: String): String { val room = getRoom(session) - // first in queue is currently playing song + // first in queue is currently playing video if (room.queue.isNotEmpty() && room.queue[0].id == queueId) { return "queue rm deny" } @@ -87,7 +111,7 @@ fun reorder(session: Session, order: String): String { fun skip(session: Session): String { val room = getRoom(session) synchronized(room.queue) { - // first in queue is currently playing song + // first in queue is currently playing video if (room.queue.size < 2) { return "skip deny" } diff --git a/backend/src/main/kotlin/de/randomerror/ytsync/Sync.kt b/backend/src/main/kotlin/de/randomerror/ytsync/Sync.kt index 34fbd0c..57be7fb 100644 --- a/backend/src/main/kotlin/de/randomerror/ytsync/Sync.kt +++ b/backend/src/main/kotlin/de/randomerror/ytsync/Sync.kt @@ -121,7 +121,7 @@ fun setEnded(session: Session, videoUrl: String): String { synchronized(room.queue) { if (room.queue.isEmpty()) return "end empty" - if (room.queue[0].source.url != videoUrl) return "end old" + if (room.queue[0].source?.url != videoUrl) return "end old" room.ignoreSkipTill = Instant.now().plusSeconds(IGNORE_DURATION) playNext(session, room) diff --git a/backend/src/main/kotlin/de/randomerror/ytsync/VideoInfo.kt b/backend/src/main/kotlin/de/randomerror/ytsync/VideoInfo.kt index 08407b5..c111f34 100644 --- a/backend/src/main/kotlin/de/randomerror/ytsync/VideoInfo.kt +++ b/backend/src/main/kotlin/de/randomerror/ytsync/VideoInfo.kt @@ -12,20 +12,41 @@ import java.lang.UnsupportedOperationException import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread -private const val YT_DLP_TIMEOUT = 5L +private const val YT_DLP_TIMEOUT = 15L private val logger = KotlinLogging.logger {} -fun getFallbackYoutubeVideo(query: String, match: String): QueueItem { +fun getInitialVideoInfo(query: String, youtubeId: String?): QueueItem { + val favicon = getInitialFavicon(query, youtubeId) + if (youtubeId !== null) { + return QueueItem( + VideoSource( + "https://www.youtube.com/watch?v=$youtubeId", + null + ), + query, + null, + "https://i.ytimg.com/vi/$youtubeId/mqdefault.jpg", + favicon, + true + ) + } + return QueueItem(null, query, null, null, favicon, true) +} + +fun getFallbackYoutubeVideo(query: String, youtubeId: String): QueueItem { return QueueItem( VideoSource( - "https://www.youtube.com/watch?v=$match", + "https://www.youtube.com/watch?v=$youtubeId", null ), query, - "Unknown video $match", - "https://i.ytimg.com/vi/$match/mqdefault.jpg" + "Unknown video $youtubeId", + "https://i.ytimg.com/vi/$youtubeId/mqdefault.jpg", + getInitialFavicon(query, youtubeId), + false ) } @@ -46,7 +67,7 @@ private fun fetchVideoInfoYouTubeOEmbed(query: String, youtubeId: String): Queue val video = JsonParser.parseString(videoData).asJsonObject val title = video.getNullable("title")?.asString val thumbnail = "https://i.ytimg.com/vi/$youtubeId/mqdefault.jpg" - QueueItem(VideoSource(query, null), query, title, thumbnail) + QueueItem(VideoSource(query, null), query, title, thumbnail, null, false) } catch (e: JsonParseException) { logger.warn("failed to parse oembed response for query $query", e) null @@ -63,7 +84,9 @@ private fun fetchVideoInfoYtDlp(youtubeId: String?, query: String): QueueItem? { val isYoutube = youtubeId != null || !query.matches(Regex("^(ftp|https?)://.*")) val process = Runtime.getRuntime().exec(buildYtDlpCommand(query, isYoutube)) val result = StringWriter() - process.inputStream.bufferedReader().copyTo(result) + val reader = thread(isDaemon = true) { + process.inputStream.bufferedReader().copyTo(result) + } if (!process.waitFor(YT_DLP_TIMEOUT, TimeUnit.SECONDS)) { logger.warn("ytdl timeout") process.destroy() @@ -74,6 +97,7 @@ private fun fetchVideoInfoYtDlp(youtubeId: String?, query: String): QueueItem? { logger.warn(process.errorStream.bufferedReader().readText()) return null } + reader.join() val videoData = result.buffer.toString() return parseYtDlpOutput(videoData, query, isYoutube) } @@ -93,7 +117,7 @@ private fun parseYtDlpOutput( val title = video.getNullable("title")?.asString val thumbnail = video.getNullable("thumbnail")?.asString val contentType = if (isYoutube) null else getContentType(url) - QueueItem(VideoSource(url, contentType), query, title, thumbnail) + QueueItem(VideoSource(url, contentType), query, title, thumbnail, null, false) } catch (e: JsonParseException) { logger.warn("failed to parse ytdlp output for query $query", e) null diff --git a/frontend/justfile b/frontend/justfile index 4bc7d60..0c09d2c 100644 --- a/frontend/justfile +++ b/frontend/justfile @@ -6,6 +6,7 @@ start: build-vendor git-submodules: git submodule init + git submodule update install-vendor: git-submodules cd vendor/react-youtube/packages/react-youtube && yarn diff --git a/frontend/package.json b/frontend/package.json index bc43423..78351a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,12 +13,12 @@ "@testing-library/user-event": "^14.1.1", "@types/jest": "^29.2.0", "@types/node": "^18.11.4", - "@types/react": "^18.0.9", - "@types/react-dom": "^18.0.3", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/sortablejs": "^1.15.0", "@types/video.js": "^7.3.44", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", "react-scripts": "^5.0.1", "react-sortablejs": "6.1.4", "react-youtube": "file:vendor/react-youtube/packages/react-youtube", diff --git a/frontend/src/component/FavIcon.tsx b/frontend/src/component/FavIcon.tsx index cdb4178..be91d57 100644 --- a/frontend/src/component/FavIcon.tsx +++ b/frontend/src/component/FavIcon.tsx @@ -2,30 +2,17 @@ import { useState } from 'react'; import QueueItem from '../model/QueueItem'; import './FavIcon.css'; -const faviconUrl = (url: string, originalQuery: string) => { - let baseUrl; - try { - baseUrl = new URL(originalQuery); - } catch (e) { - baseUrl = new URL(url); - } - baseUrl.search = ''; - baseUrl.pathname = 'favicon.ico'; - return baseUrl; -}; - interface FavIconProps { item: QueueItem; } function FavIcon({ item }: FavIconProps) { const [error, setError] = useState(false); - const favicon = faviconUrl(item.source.url, item.originalQuery); - return error ? null : ( + return error || !item.favicon ? null : ( {`favicon setError(true)} > ); diff --git a/frontend/src/component/Input.tsx b/frontend/src/component/Input.tsx index ed5ab2b..69ab277 100644 --- a/frontend/src/component/Input.tsx +++ b/frontend/src/component/Input.tsx @@ -1,14 +1,13 @@ import React, { useState } from 'react'; import './Input.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCircleNotch, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faPlus } from '@fortawesome/free-solid-svg-icons'; interface InputProps { addToQueue: (input: string) => void; - working: boolean; } -function Input({ addToQueue, working }: InputProps) { +function Input({ addToQueue }: InputProps) { const [input, setInput] = useState(''); const onKey = (e: React.KeyboardEvent) => { @@ -42,12 +41,8 @@ function Input({ addToQueue, working }: InputProps) { className="input-send" onClick={send} aria-label="Add to queue" - disabled={working} > - + ); diff --git a/frontend/src/component/Queue.tsx b/frontend/src/component/Queue.tsx index c41b8b0..a020483 100644 --- a/frontend/src/component/Queue.tsx +++ b/frontend/src/component/Queue.tsx @@ -10,12 +10,18 @@ import { WebsocketContext } from '../context/websocket'; import FavIcon from './FavIcon'; import Thumbnail from './Thumbnail'; -const getDomain = (item: QueueItem) => { +const getDomain: (item: QueueItem) => string | null = (item: QueueItem) => { let baseUrl; try { baseUrl = new URL(item.originalQuery); } catch (e) { - baseUrl = new URL(item.source.url); + if (item.source) { + baseUrl = new URL(item.source?.url); + } else if (item.favicon) { + baseUrl = new URL(item.favicon); + } else { + return null; + } } const host = baseUrl.hostname; return host.replace(/^www./, ''); @@ -23,21 +29,37 @@ const getDomain = (item: QueueItem) => { interface QueueProps { addNotification: (notification: Notification) => void; - setWorking: (working: boolean) => void; } -function Queue({ addNotification, setWorking }: QueueProps) { +function Queue({ addNotification }: QueueProps) { const [queue, setQueue] = useState([]); useWebsocketMessages( (msg: string) => { - if (msg.startsWith('video')) { - setWorking(false); - } else if (msg.startsWith('queue add')) { + if (msg.startsWith('queue add')) { const msgParts = msg.split(' '); const queueItem: QueueItem = JSON.parse(msgParts.slice(2).join(' ')); - setQueue((queue) => [...queue, queueItem]); - setWorking(false); + setQueue((queue) => { + const index = queue.findIndex((video) => video.id === queueItem.id); + console.log({ index }); + if (index === -1) { + let newQueue = [...queue, queueItem]; + newQueue.sort((a, b) => +a.loading - +b.loading); + return newQueue; + } else { + if (queue[index].loading && !queueItem.loading) { + let newQueue = [...queue]; + newQueue.splice(index, 1); + newQueue.push(queueItem); + newQueue.sort((a, b) => +a.loading - +b.loading); + return newQueue; + } else { + let newQueue = [...queue]; + newQueue.splice(index, 1, queueItem); + return newQueue; + } + } + }); } else if (msg.startsWith('queue rm')) { const id = msg.split(' ')[2]; setQueue((queue) => queue.filter((video) => video.id !== id)); @@ -46,6 +68,9 @@ function Queue({ addNotification, setWorking }: QueueProps) { setQueue((queue) => { const sortedQueue = [...queue]; sortedQueue.sort((a, b) => { + if (a.loading !== b.loading) { + return +a.loading - +b.loading; + } return order.indexOf(a.id) - order.indexOf(b.id); }); return sortedQueue; @@ -56,25 +81,27 @@ function Queue({ addNotification, setWorking }: QueueProps) { level: 'info', permanent: false, }); - setWorking(false); } else if (msg === 'queue err duplicate') { addNotification({ message: 'Already in queue', level: 'info', permanent: false, }); - setWorking(false); } }, - [addNotification, setWorking], + [addNotification], ); const { sendMessage } = useContext(WebsocketContext); const skip = () => sendMessage('skip'); const reorderQueue = useCallback( (videos: QueueItem[]) => { - const oldOrder = queue.map((video) => video.id); - const newOrder = videos.map((video) => video.id); + const oldOrder = queue + .filter((video) => !video.loading) + .map((video) => video.id); + const newOrder = videos + .filter((video) => !video.loading) + .map((video) => video.id); if ( oldOrder.length !== newOrder.length || [...oldOrder].sort().join() !== [...newOrder].sort().join() || @@ -97,7 +124,7 @@ function Queue({ addNotification, setWorking }: QueueProps) {

Queue

- {queue.length === 0 ? null : ( + {queue.filter(video => !video.loading).length === 0 ? null : ( @@ -113,17 +140,28 @@ function Queue({ addNotification, setWorking }: QueueProps) { return (
  • - +
    -
    {item.title || 'No title'}
    +
    + {item.title || (item.loading ? 'Loading...' : 'No title')} +
    - {getDomain(item)} + {' '} + {getDomain(item) || ''}
    - + {!item.loading ? ( + + ) : null}
  • ); })} diff --git a/frontend/src/component/Sidebar.tsx b/frontend/src/component/Sidebar.tsx index 55f526b..07c604e 100644 --- a/frontend/src/component/Sidebar.tsx +++ b/frontend/src/component/Sidebar.tsx @@ -3,7 +3,7 @@ import Input from './Input'; import Queue from './Queue'; import Notification from '../model/Notification'; import Infobox from './Infobox'; -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext } from 'react'; import { WebsocketContext } from '../context/websocket'; interface SidebarProps { @@ -12,13 +12,11 @@ interface SidebarProps { } function Sidebar({ notifications, addNotification }: SidebarProps) { - const [working, setWorking] = useState(false); const { sendMessage } = useContext(WebsocketContext); const addToQueue = useCallback( (input: string) => { sendMessage(`queue add ${input}`); - setWorking(true); }, [sendMessage], ); @@ -29,8 +27,8 @@ function Sidebar({ notifications, addNotification }: SidebarProps) { notifications={notifications} addNotification={addNotification} /> - - + +
    ); } diff --git a/frontend/src/component/Thumbnail.tsx b/frontend/src/component/Thumbnail.tsx index 158c5c5..dba0557 100644 --- a/frontend/src/component/Thumbnail.tsx +++ b/frontend/src/component/Thumbnail.tsx @@ -1,18 +1,31 @@ -import { faImage, faSlash } from '@fortawesome/free-solid-svg-icons'; +import { + faCircleNotch, + faImage, + faSlash, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import './Thumbnail.css'; interface ThumbnailProps { thumbnailUrl: string | null; + loading: boolean; } -function Thumbnail({ thumbnailUrl }: ThumbnailProps) { +function Thumbnail({ thumbnailUrl, loading }: ThumbnailProps) { return thumbnailUrl === null ? (
    - - - - + {loading ? ( + + ) : ( + + + + + )}
    ) : ( diff --git a/frontend/src/hook/websocket-messages.ts b/frontend/src/hook/websocket-messages.ts index 83fd2c4..43e2478 100644 --- a/frontend/src/hook/websocket-messages.ts +++ b/frontend/src/hook/websocket-messages.ts @@ -3,7 +3,6 @@ import { useCallback, useContext, useEffect, - useId, } from 'react'; import { WebsocketContext } from '../context/websocket'; @@ -11,7 +10,7 @@ export function useWebsocketMessages( handler: (msg: string) => void, deps: DependencyList, ) { - const id = useId(); + const id = handler.toString(); const { addMessageCallback, removeMessageCallback } = useContext(WebsocketContext); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index d1cec82..811d417 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,16 +1,11 @@ import React from 'react'; -import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +import { render } from 'react-dom'; const container = document.getElementById('root'); -const root = createRoot(container!); -root.render( - - - , -); +render(, container); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/frontend/src/model/QueueItem.ts b/frontend/src/model/QueueItem.ts index bdf4812..bf595d9 100644 --- a/frontend/src/model/QueueItem.ts +++ b/frontend/src/model/QueueItem.ts @@ -1,9 +1,11 @@ import { VideoSource } from '../component/Player'; export default interface QueueItem { - source: VideoSource; + source?: VideoSource; originalQuery: string; title?: string; thumbnail?: string; + favicon?: string; + loading: boolean; id: string; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 65cec8d..71c1135 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2078,14 +2078,21 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.3": +"@types/react-dom@^17.0.0": + version "17.0.19" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.19.tgz#36feef3aa35d045cacd5ed60fe0eef5272f19492" + integrity sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ== + dependencies: + "@types/react" "^17" + +"@types/react-dom@^18.0.0": version "18.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.0.9": +"@types/react@*": version "18.0.28" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== @@ -2094,6 +2101,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17", "@types/react@^17.0.0": + version "17.0.58" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.58.tgz#c8bbc82114e5c29001548ebe8ed6c4ba4d3c9fb0" + integrity sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -7665,13 +7681,14 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@^18.1.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +react-dom@^17.0.0: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" - scheduler "^0.23.0" + object-assign "^4.1.1" + scheduler "^0.20.2" react-error-overlay@^6.0.11: version "6.0.11" @@ -7768,12 +7785,13 @@ react-sortablejs@6.1.4: prop-types "15.8.1" youtube-player "5.5.2" -react@^18.1.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== +react@^17.0.0: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" + object-assign "^4.1.1" read-cache@^1.0.0: version "1.0.0" @@ -8069,12 +8087,13 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" + object-assign "^4.1.1" schema-utils@2.7.0: version "2.7.0" diff --git a/nginx-docker.conf b/nginx-docker.conf index a8b2a49..6fe339a 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -12,7 +12,7 @@ server { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; - proxy_pass http://ytsync-backend:4567/api/; + proxy_pass http://ytsync-backend:4567/; } location / {