diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63a54a471c7c6..569ded4476692 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,91 +108,91 @@ jobs: run: yarn run build:arm64 - name: Upload Linux .zip x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64 path: build/freetube-${{ steps.versionNumber.outputs.result }}.zip - name: Upload Linux .7z x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}.7z - name: Upload Linux .zip ARMv7l Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.zip - name: Upload Linux .7z ARMv7l Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.7z - name: Upload Linux .zip ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64 path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.zip - name: Upload Linux .7z ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.7z - name: Upload .deb x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb path: build/freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb - name: Upload .deb ARMv7l Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') with: name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb path: build/freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb - name: Upload .deb ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb path: build/freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb - name: Upload AppImage x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.AppImage path: build/FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage - name: Upload AppImage ARMv7l Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') with: name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.AppImage path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-armv7l.AppImage - name: Upload AppImage ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.AppImage path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-arm64.AppImage - name: Upload .rpm x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.rpm @@ -201,133 +201,133 @@ jobs: # rpm are not built for armv7l - name: Upload .rpm ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.rpm path: build/freetube-${{ steps.versionNumber.outputs.result }}.aarch64.rpm - name: Upload Alpine .apk x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_amd64.apk path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk - name: Upload Alpine .apk ARMv7l Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l') with: name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_armv7l.apk path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.apk - name: Upload Alpine .apk ARM64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_arm64.apk path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.apk - name: Upload Pacman .pacman x64 Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') with: name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.pacman path: build/freetube-${{ steps.versionNumber.outputs.result }}.pacman # - name: Upload Web Build - # uses: actions/upload-artifact@v3 + # uses: actions/upload-artifact@v4 # if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64') # with: # name: freetube_${{ steps.versionNumber.outputs.result }}_static_web # path: dist/web - name: Upload Windows x64 .exe Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-setup-x64.exe path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe - name: Upload Windows arm64 .exe Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-setup-arm64.exe path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe - name: Upload Windows x64 .zip Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.zip - name: Upload Windows x64 .7z Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.7z - name: Upload Windows arm64 .zip Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.zip - name: Upload Windows arm64 .7z Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.7z - name: Upload Windows x64 Portable Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-portable-x64.exe path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe - name: Upload Windows arm64 Portable Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-portable-arm64.exe path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe - name: Upload Mac x64 .dmg Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.dmg path: build/freetube-${{ steps.versionNumber.outputs.result }}.dmg # - name: Upload Mac arm64 .dmg Artifact -# uses: actions/upload-artifact@v3 +# uses: actions/upload-artifact@v4 # if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') # with: # name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.dmg # path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.dmg - name: Upload Mac x64 .zip Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.zip path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.zip - name: Upload Mac x64 .7z Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64') with: name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.7z path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.7z # - name: Upload Mac arm64 .zip Artifact -# uses: actions/upload-artifact@v3 +# uses: actions/upload-artifact@v4 # if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64') # with: # name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.zip diff --git a/jsconfig.json b/jsconfig.json index 8f5ea8a228e3c..6efa53c23ad98 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,8 @@ { "vueCompilerOptions": { "target": 2.7 + }, + "compilerOptions": { + "strictNullChecks": true } } diff --git a/package.json b/package.json index 3a6bdfb639834..fe8d745cce1e7 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "marked": "^11.2.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "swiper": "^11.0.5", + "swiper": "^11.0.6", "video.js": "7.21.5", "videojs-contrib-quality-levels": "^3.0.0", "videojs-http-source-selector": "^1.1.6", @@ -80,7 +80,7 @@ "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vuex": "^3.6.2", - "youtubei.js": "^8.2.0" + "youtubei.js": "^9.0.2" }, "devDependencies": { "@babel/core": "^7.23.9", @@ -92,7 +92,7 @@ "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.10.0", "css-minimizer-webpack-plugin": "^6.0.0", - "electron": "^28.2.1", + "electron": "^28.2.2", "electron-builder": "^24.9.1", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -102,7 +102,7 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-unicorn": "^50.0.1", + "eslint-plugin-unicorn": "^51.0.1", "eslint-plugin-vue": "^9.21.1", "eslint-plugin-vuejs-accessibility": "^2.2.1", "eslint-plugin-yml": "^1.12.2", @@ -112,7 +112,7 @@ "lefthook": "^1.6.1", "mini-css-extract-plugin": "^2.8.0", "npm-run-all": "^4.1.5", - "postcss": "^8.4.33", + "postcss": "^8.4.35", "postcss-scss": "^4.0.9", "prettier": "^2.8.8", "rimraf": "^5.0.5", diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index 81722ac93c613..376c7b0561786 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -294,7 +294,9 @@ export async function getLocalChannelVideos(id) { // if the channel doesn't have a videos tab, YouTube returns the home tab instead // so we need to check that we got the right tab if (videosTab.current_tab?.endpoint.metadata.url?.endsWith('/videos')) { - return parseLocalChannelVideos(videosTab.videos, videosTab.header.author) + const { id: channelId = id, name } = parseLocalChannelHeader(videosTab) + + return parseLocalChannelVideos(videosTab.videos, channelId, name) } else { return [] } @@ -324,7 +326,9 @@ export async function getLocalChannelLiveStreams(id) { // if the channel doesn't have a live tab, YouTube returns the home tab instead // so we need to check that we got the right tab if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) { - return parseLocalChannelVideos(liveStreamsTab.videos, liveStreamsTab.header.author) + const { id: channelId = id, name } = parseLocalChannelHeader(liveStreamsTab) + + return parseLocalChannelVideos(liveStreamsTab.videos, channelId, name) } else { return [] } @@ -368,17 +372,159 @@ export async function getLocalChannelCommunity(id) { } } +/** + * @param {YT.Channel} channel + */ +export function parseLocalChannelHeader(channel) { + /** @type {string=} */ + let id + /** @type {string} */ + let name + /** @type {string=} */ + let thumbnailUrl + /** @type {string=} */ + let bannerUrl + /** @type {string=} */ + let subscriberText + /** @type {string[]} */ + const tags = [] + + switch (channel.header.type) { + case 'C4TabbedHeader': { + // example: Linus Tech Tips + // https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw + + /** + * @type {import('youtubei.js').YTNodes.C4TabbedHeader} + */ + const header = channel.header + + id = header.author.id + name = header.author.name + thumbnailUrl = header.author.best_thumbnail.url + bannerUrl = header.banner?.[0]?.url + subscriberText = header.subscribers?.text + break + } + case 'CarouselHeader': { + // examples: Music and YouTube Gaming + // https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ + // https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg + + /** + * @type {import('youtubei.js').YTNodes.CarouselHeader} + */ + const header = channel.header + + /** + * @type {import('youtubei.js').YTNodes.TopicChannelDetails} + */ + const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails') + name = topicChannelDetails.title.text + subscriberText = topicChannelDetails.subtitle.text + thumbnailUrl = topicChannelDetails.avatar[0].url + + if (channel.metadata.external_id) { + id = channel.metadata.external_id + } else { + id = topicChannelDetails.subscribe_button.channel_id + } + break + } + case 'InteractiveTabbedHeader': { + // example: Minecraft - Topic + // https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg + + /** + * @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader} + */ + const header = channel.header + name = header.title.text + thumbnailUrl = header.box_art.at(-1).url + bannerUrl = header.banner[0]?.url + + const badges = header.badges.map(badge => badge.label).filter(tag => tag) + tags.push(...badges) + + id = channel.current_tab?.endpoint.payload.browseId + break + } + case 'PageHeader': { + // example: YouTube Gaming + // https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg + + // User channels (an A/B test at the time of writing) + + /** + * @type {import('youtubei.js').YTNodes.PageHeader} + */ + const header = channel.header + + name = header.content.title.text.text + if (header.content.image) { + if (header.content.image.type === 'ContentPreviewImageView') { + /** @type {import('youtubei.js').YTNodes.ContentPreviewImageView} */ + const image = header.content.image + + thumbnailUrl = image.image[0].url + } else { + /** @type {import('youtubei.js').YTNodes.DecoratedAvatarView} */ + const image = header.content.image + thumbnailUrl = image.avatar?.image[0].url + } + } + + if (!thumbnailUrl && channel.metadata.thumbnail) { + thumbnailUrl = channel.metadata.thumbnail[0].url + } + + if (header.content.banner) { + bannerUrl = header.content.banner.image[0]?.url + } + + if (header.content.actions) { + const modal = header.content.actions.actions_rows[0].actions[0].on_tap.modal + + if (modal && modal.type === 'ModalWithTitleAndButton') { + /** @type {import('youtubei.js').YTNodes.ModalWithTitleAndButton} */ + const typedModal = modal + + id = typedModal.button.endpoint.next_endpoint?.payload.browseId + } + } else if (channel.metadata.external_id) { + id = channel.metadata.external_id + } + + if (header.content.metadata) { + subscriberText = header.content.metadata.metadata_rows[0].metadata_parts[1].text.text + } + + break + } + } + + return { + id, + name, + thumbnailUrl, + bannerUrl, + subscriberText, + tags + } +} + /** * @param {import('youtubei.js').YTNodes.Video[]} videos - * @param {Misc.Author} author + * @param {string} channelId + * @param {string} channelName */ -export function parseLocalChannelVideos(videos, author) { +export function parseLocalChannelVideos(videos, channelId, channelName) { const parsedVideos = videos.map(parseLocalListVideo) // fix empty author info parsedVideos.forEach(video => { - video.author = author.name - video.authorId = author.id + video.author = channelName + video.authorId = channelId }) return parsedVideos @@ -386,16 +532,17 @@ export function parseLocalChannelVideos(videos, author) { /** * @param {import('youtubei.js').YTNodes.ReelItem[]} shorts - * @param {Misc.Author} author + * @param {string} channelId + * @param {string} channelName */ -export function parseLocalChannelShorts(shorts, author) { +export function parseLocalChannelShorts(shorts, channelId, channelName) { return shorts.map(short => { return { type: 'video', videoId: short.id, title: short.title.text, - author: author.name, - authorId: author.id, + author: channelName, + authorId: channelId, viewCount: parseLocalSubscriberCount(short.views.text), lengthSeconds: '' } @@ -409,40 +556,43 @@ export function parseLocalChannelShorts(shorts, author) { /** * @param {Playlist|GridPlaylist} playlist - * @param {Misc.Author} author + * @param {string} channelId + * @param {string} chanelName */ -export function parseLocalListPlaylist(playlist, author = undefined) { - let channelName - let channelId = null - /** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */ - const thumbnailRenderer = playlist.thumbnail_renderer +export function parseLocalListPlaylist(playlist, channelId = undefined, channelName = undefined) { + let internalChannelName + let internalChannelId = null + if (playlist.author && playlist.author.id !== 'N/A') { if (playlist.author instanceof Misc.Text) { - channelName = playlist.author.text + internalChannelName = playlist.author.text - if (author) { - channelId = author.id + if (channelId) { + internalChannelId = channelId } } else { - channelName = playlist.author.name - channelId = playlist.author.id + internalChannelName = playlist.author.name + internalChannelId = playlist.author.id } - } else if (author) { - channelName = author.name - channelId = author.id + } else if (channelId || channelName) { + internalChannelName = channelName + internalChannelId = channelId } else if (playlist.author?.name) { // auto-generated album playlists don't have an author // so in search results, the author text is "Playlist" and doesn't have a link or channel ID - channelName = playlist.author.name + internalChannelName = playlist.author.name } + /** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */ + const thumbnailRenderer = playlist.thumbnail_renderer + return { type: 'playlist', dataSource: 'local', title: playlist.title.text, thumbnail: thumbnailRenderer ? thumbnailRenderer.thumbnail[0].url : playlist.thumbnails[0].url, - channelName, - channelId, + channelName: internalChannelName, + channelId: internalChannelId, playlistId: playlist.id, videoCount: extractNumberFromString(playlist.video_count.text) } diff --git a/src/renderer/views/Channel/Channel.css b/src/renderer/views/Channel/Channel.css index e2b9b521360b6..a1324d2bb8237 100644 --- a/src/renderer/views/Channel/Channel.css +++ b/src/renderer/views/Channel/Channel.css @@ -217,6 +217,23 @@ } } +@media only screen and (max-width: 680px) { + .channelInfo { + flex-direction: column; + margin-block: 20px 10px; + } + .card { + max-inline-size: none; + inline-size: 100%; + } + .channelInfoActionsContainer { + flex-direction: row-reverse; + justify-content: left; + gap: 10px; + margin-block-start: 5px; + } +} + @media only screen and (max-width: 400px) { .channelInfo { justify-content: center; @@ -224,7 +241,11 @@ } .channelInfoActionsContainer { - flex-direction: column-reverse; + justify-content: center; + } + + .channelLineContainer { + padding-inline-start: 0; } .thumbnailContainer { diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js index 0fedac2c90f4c..3fea114b788a0 100644 --- a/src/renderer/views/Channel/Channel.js +++ b/src/renderer/views/Channel/Channel.js @@ -25,6 +25,7 @@ import { import { getLocalChannel, getLocalChannelId, + parseLocalChannelHeader, parseLocalChannelShorts, parseLocalChannelVideos, parseLocalCommunityPosts, @@ -532,90 +533,22 @@ export default defineComponent({ return } - let channelId - let subscriberText = null - let tags = [] + const parsedHeader = parseLocalChannelHeader(channel) - switch (channel.header.type) { - case 'C4TabbedHeader': { - // example: Linus Tech Tips - // https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw + const channelId = parsedHeader.id ?? this.id + const subscriberText = parsedHeader.subscriberText ?? null + let tags = parsedHeader.tags - /** - * @type {import('youtubei.js').YTNodes.C4TabbedHeader} - */ - const header = channel.header + channelThumbnailUrl = parsedHeader.thumbnailUrl ?? this.subscriptionInfo?.thumbnail + channelName = parsedHeader.name ?? this.subscriptionInfo?.name - channelId = header.author.id - channelName = header.author.name - channelThumbnailUrl = header.author.best_thumbnail.url - subscriberText = header.subscribers?.text - break - } - case 'CarouselHeader': { - // examples: Music and YouTube Gaming - // https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ - // https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg - - /** - * @type {import('youtubei.js').YTNodes.CarouselHeader} - */ - const header = channel.header - - /** - * @type {import('youtubei.js').YTNodes.TopicChannelDetails} - */ - const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails') - channelName = topicChannelDetails.title.text - subscriberText = topicChannelDetails.subtitle.text - channelThumbnailUrl = topicChannelDetails.avatar[0].url - - if (channel.metadata.external_id) { - channelId = channel.metadata.external_id - } else { - channelId = topicChannelDetails.subscribe_button.channel_id - } - break - } - case 'InteractiveTabbedHeader': { - // example: Minecraft - Topic - // https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg - - /** - * @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader} - */ - const header = channel.header - channelName = header.title.text - channelId = this.id - channelThumbnailUrl = header.box_art.at(-1).url - - const badges = header.badges.map(badge => badge.label).filter(tag => tag) - tags.push(...badges) - break - } - case 'PageHeader': { - // example: YouTube Gaming (an A/B test at the time of writing) - // https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg - - /** - * @type {import('youtubei.js').YTNodes.PageHeader} - */ - const header = channel.header - - channelName = header.content.title.text - channelThumbnailUrl = header.content.image.image[0].url - channelId = this.id - - break - } - } - - if (channelThumbnailUrl.startsWith('//')) { + if (channelThumbnailUrl?.startsWith('//')) { channelThumbnailUrl = `https:${channelThumbnailUrl}` } this.channelName = channelName this.thumbnailUrl = channelThumbnailUrl + this.bannerUrl = parsedHeader.bannerUrl ?? null this.isFamilyFriendly = !!channel.metadata.is_family_safe if (channel.metadata.tags) { @@ -646,12 +579,6 @@ export default defineComponent({ this.updateSubscriptionDetails({ channelThumbnailUrl, channelName, channelId }) - if (channel.header.banner?.length > 0) { - this.bannerUrl = channel.header.banner[0].url - } else { - this.bannerUrl = null - } - let relatedChannels = channel.channels.map(({ author }) => ({ name: author.name, id: author.id, @@ -837,7 +764,7 @@ export default defineComponent({ return } - this.latestVideos = parseLocalChannelVideos(videosTab.videos, channel.header.author) + this.latestVideos = parseLocalChannelVideos(videosTab.videos, this.id, this.channelName) this.videoContinuationData = videosTab.has_continuation ? videosTab : null this.isElementListLoading = false } catch (err) { @@ -862,7 +789,7 @@ export default defineComponent({ */ const continuation = await this.videoContinuationData.getContinuation() - this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author)) + this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.id, this.channelName)) this.videoContinuationData = continuation.has_continuation ? continuation : null } catch (err) { console.error(err) @@ -895,7 +822,7 @@ export default defineComponent({ return } - this.latestShorts = parseLocalChannelShorts(shortsTab.videos, channel.header.author) + this.latestShorts = parseLocalChannelShorts(shortsTab.videos, this.id, this.channelName) this.shortContinuationData = shortsTab.has_continuation ? shortsTab : null this.isElementListLoading = false } catch (err) { @@ -920,7 +847,7 @@ export default defineComponent({ */ const continuation = await this.shortContinuationData.getContinuation() - this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.channelInstance.header.author)) + this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.id, this.channelName)) this.shortContinuationData = continuation.has_continuation ? continuation : null } catch (err) { console.error(err) @@ -953,7 +880,7 @@ export default defineComponent({ return } - this.latestLive = parseLocalChannelVideos(liveTab.videos, channel.header.author) + this.latestLive = parseLocalChannelVideos(liveTab.videos, this.id, this.channelName) this.liveContinuationData = liveTab.has_continuation ? liveTab : null this.isElementListLoading = false } catch (err) { @@ -978,7 +905,7 @@ export default defineComponent({ */ const continuation = await this.liveContinuationData.getContinuation() - this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author)) + this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.id, this.channelName)) this.liveContinuationData = continuation.has_continuation ? continuation : null } catch (err) { console.error(err) @@ -1270,7 +1197,7 @@ export default defineComponent({ return } - this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author)) + this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.playlistContinuationData = playlistsTab.has_continuation ? playlistsTab : null this.isElementListLoading = false } catch (err) { @@ -1295,7 +1222,7 @@ export default defineComponent({ */ const continuation = await this.playlistContinuationData.getContinuation() - const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author)) + const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.latestPlaylists = this.latestPlaylists.concat(parsedPlaylists) this.playlistContinuationData = continuation.has_continuation ? continuation : null } catch (err) { @@ -1393,7 +1320,7 @@ export default defineComponent({ return } - this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author)) + this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null this.isElementListLoading = false } catch (err) { @@ -1418,7 +1345,7 @@ export default defineComponent({ */ const continuation = await this.releaseContinuationData.getContinuation() - const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author)) + const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.latestReleases = this.latestReleases.concat(parsedReleases) this.releaseContinuationData = continuation.has_continuation ? continuation : null } catch (err) { @@ -1506,7 +1433,7 @@ export default defineComponent({ return } - this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author)) + this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.podcastContinuationData = podcastTab.has_continuation ? podcastTab : null this.isElementListLoading = false } catch (err) { @@ -1531,7 +1458,7 @@ export default defineComponent({ */ const continuation = await this.podcastContinuationData.getContinuation() - const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author)) + const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName)) this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts) this.releaseContinuationData = continuation.has_continuation ? continuation : null } catch (err) { @@ -1857,7 +1784,7 @@ export default defineComponent({ if (item.type === 'Video') { return parseLocalListVideo(item) } else { - return parseLocalListPlaylist(item, this.channelInstance.header.author) + return parseLocalListPlaylist(item, this.id, this.channelName) } }) diff --git a/static/locales/nl.yaml b/static/locales/nl.yaml index 43b440c100822..007e4053061f0 100644 --- a/static/locales/nl.yaml +++ b/static/locales/nl.yaml @@ -135,12 +135,35 @@ User Playlists: LatestCreatedFirst: On­langs aan­gemaakt NameDescending: Z - A NameAscending: A - Z + EarliestPlayedFirst: Eerst gespeeld + EarliestCreatedFirst: Eerst aangemaakt + LatestUpdatedFirst: Laatst bijgewerkt + EarliestUpdatedFirst: Eerst bijgewerkt + LatestPlayedFirst: Meest recent afgespeeld CreatePlaylistPrompt: Create: Aanmaken + New Playlist Name: Naam voor nieuwe afspeel­lijst AddVideoPrompt: Save: Opslaan + N playlists selected: '{playlistCount} geselecteerd' + Search in Playlists: Zoeken in afspeel­lijsten Save Changes: Wijzigingen opslaan Copy Playlist: Afspeel­lijst kopiëren + Create New Playlist: Nieuwe afspeel­lijst aanmaken + Add to Playlist: Toevoegen aan afspeel­lijst + Move Video Up: Video omhoog verplaatsen + Move Video Down: Video omlaag verplaatsen + Remove from Playlist: Verwijderen uit afspeellijst + Edit Playlist Info: Afspeel­lijst­info bewerken + Remove Watched Videos: Bekeken video's verwijderen + Add to Favorites: Toevoegen aan {playlistName} + Remove from Favorites: Verwijderen uit {playlistName} + Disable Quick Bookmark: Snelle bladwijzers uitschakelen + SinglePlaylistView: + Toast: + Video has been removed: Video is verwijderd + Quick bookmark disabled: Snelle bladwijzers uitgeschakeld + Playlist has been updated.: De afspeel­lijst is bijgewerkt. History: # On History Page History: 'Geschiedenis' @@ -209,6 +232,7 @@ Settings: Catppuccin Mocha: Catppuccin-mokka Pastel Pink: Pastel­roze Hot Pink: Heet-roze + Nordic: Noords Main Color Theme: Main Color Theme: 'Primaire themakleur' Red: 'Rood' @@ -333,6 +357,7 @@ Settings: verwijderen Save Watched Videos With Last Viewed Playlist: Houd bekeken video's bij met de afspeellijst ‘Laatst bekeken’ + Remove All Playlists: Alle afspeel­lijsten verwijderen Subscription Settings: Subscription Settings: 'Abonnement­instellingen' Hide Videos on Watch: 'Bekeken video''s verbergen' @@ -501,6 +526,7 @@ Settings: Players: None: Name: Geen + Ignore Default Arguments: Standaard­argumenten negeren Download Settings: Choose Path: Pad kiezen Download Settings: Downloadinstellingen @@ -529,6 +555,7 @@ Settings: Password Incorrect: Wachtwoord onjuist Unlock: Ontgrendelen Enter Password To Unlock: Voer wachtwoord in om instellingen te ontgrendelen + Expand All Settings Sections: Alle instellingen­secties uitvouwen About: #On About page About: 'Over' @@ -937,6 +964,10 @@ Profile: Profile Settings: Profielinstellingen Toggle Profile List: Profiellijst omschakelen Profile Name: Profiel­naam + Edit Profile Name: Profiel­naam wijzigen + Create Profile Name: Profiel­naam aanmaken + Close Profile Dropdown: Profiel­menu sluiten + Open Profile Dropdown: Profiel­menu openen A new blog is now available, {blogTitle}. Click to view more: Een nieuwe blogpost is beschikbaar, {blogTitle}. Klik voor meer informatie Download From Site: Van website downloaden diff --git a/yarn.lock b/yarn.lock index 47fe00b13926b..297293d858fe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3419,10 +3419,10 @@ electron-to-chromium@^1.4.648: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== -electron@^28.2.1: - version "28.2.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.1.tgz#8edf2be24d97160b7eb52b7ce9a2424cf14c0791" - integrity sha512-wlzXf+OvOiVlBf9dcSeMMf7Q+N6DG+wtgFbMK0sA/JpIJcdosRbLMQwLg/LTwNVKIbmayqFLDp4FmmFkEMhbYA== +electron@^28.2.2: + version "28.2.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41" + integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" @@ -3777,10 +3777,10 @@ eslint-plugin-promise@^6.1.1: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== -eslint-plugin-unicorn@^50.0.1: - version "50.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz#e539cdb02dfd893c603536264c4ed9505b70e3bf" - integrity sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA== +eslint-plugin-unicorn@^51.0.1: + version "51.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz#3641c5e110324c3739d6cb98fc1b99ada39f477b" + integrity sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw== dependencies: "@babel/helper-validator-identifier" "^7.22.20" "@eslint-community/eslint-utils" "^4.4.0" @@ -6745,10 +6745,10 @@ postcss@^7.0.36: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.4.14, postcss@^8.4.32, postcss@^8.4.33: - version "8.4.33" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" - integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== +postcss@^8.4.14, postcss@^8.4.32, postcss@^8.4.33, postcss@^8.4.35: + version "8.4.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" + integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" @@ -7939,10 +7939,10 @@ svgo@^3.2.0: csso "^5.0.5" picocolors "^1.0.0" -swiper@^11.0.5: - version "11.0.5" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.0.5.tgz#6ed1ad06e6906ba42fd4b93d4988f0626a49046e" - integrity sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w== +swiper@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.0.6.tgz#787187e2711b01301b4c67b86b8e03099e2fc9f2" + integrity sha512-W/Me7MQl5rNgdM5x9i3Gll76WsyVpnHn91GhBOyL7RCFUeg62aVnflzlVfIpXzZ/87FtJOfAoDH79ZH2Yq76zA== synckit@^0.6.0: version "0.6.2" @@ -8855,10 +8855,10 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -youtubei.js@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-8.2.0.tgz#5b173f41fbe6240bb44cb733ce2c1f24e0b072ca" - integrity sha512-i/F4PEURSQmSYCQCo4dWKxOCZXhqkgAuGzNG2RUCtGSmlMX8TvwNewVD/JBjH/czdNmh9SJ00onNZMMxHbt+YA== +youtubei.js@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.0.2.tgz#77592a1144cdd51bb4258472265f5031b3966162" + integrity sha512-D7GoJmupYaJxTNQyHRWYw8MUdQTxRaa3c7nzM9etWQjaexepFGVlVtwl3CybLx7GopBNtBvr7RxSUUIUyNnYIg== dependencies: jintr "^1.1.0" tslib "^2.5.0"