Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate search to YouTube.js #3028

Merged
merged 2 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 0 additions & 7 deletions _scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,6 @@ const config = {
'./dist/**/*',
'!dist/web/*',
'!node_modules/**/*',

// renderer
'node_modules/{miniget,ytsr}/**/*',
ChunkyProgrammer marked this conversation as resolved.
Show resolved Hide resolved

'!**/README.md',
'!**/*.js.map',
'!**/*.d.ts',
],
dmg: {
contents: [
Expand Down
4 changes: 0 additions & 4 deletions _scripts/webpack.renderer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ const config = {
path: path.join(__dirname, '../dist'),
filename: '[name].js',
},
// webpack spits out errors while inlining ytsr as
// they dynamically import their package.json file to extract the bug report URL
// the error: "Critical dependency: the request of a dependency is an expression"
externals: ['ytsr'],
module: {
rules: [
{
Expand Down
3 changes: 1 addition & 2 deletions _scripts/webpack.web.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ const config = {
},
externals: {
electron: '{}',
'youtubei.js': '{}',
ytsr: '{}'
'youtubei.js': '{}'
},
module: {
rules: [
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@
"youtubei.js": "^2.7.0",
"yt-channel-info": "^3.2.1",
"yt-dash-manifest-generator": "1.1.0",
"ytdl-core": "https://github.com/absidue/node-ytdl-core#fix-likes-extraction",
"ytsr": "^3.8.0"
"ytdl-core": "https://github.com/absidue/node-ytdl-core#fix-likes-extraction"
},
"devDependencies": {
"@babel/core": "^7.20.7",
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/components/ft-list-channel/ft-list-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default Vue.extend({
channelName: '',
subscriberCount: 0,
videoCount: '',
handle: null,
uploadedTime: '',
description: ''
}
Expand All @@ -39,15 +40,15 @@ export default Vue.extend({
}
},
mounted: function () {
if (typeof (this.data.avatars) !== 'undefined') {
if (this.data.dataSource === 'local' || typeof (this.data.avatars) !== 'undefined') {
this.parseLocalData()
} else {
this.parseInvidiousData()
}
},
methods: {
parseLocalData: function () {
this.thumbnail = this.data.bestAvatar.url
this.thumbnail = this.data.thumbnail ?? this.data.bestAvatar.url

if (!this.thumbnail.includes('https:')) {
this.thumbnail = `https:${this.thumbnail}`
Expand All @@ -66,6 +67,10 @@ export default Vue.extend({
this.videoCount = Intl.NumberFormat(this.currentLocale).format(this.data.videos)
}

if (this.data.handle) {
this.handle = this.data.handle
}

this.description = this.data.descriptionShort
},

Expand Down
5 changes: 5 additions & 0 deletions src/renderer/components/ft-list-channel/ft-list-channel.scss
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
@use '../../scss-partials/_ft-list-item';

.handle {
color: inherit;
text-decoration: none;
}
8 changes: 8 additions & 0 deletions src/renderer/components/ft-list-channel/ft-list-channel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@
>
{{ subscriberCount }} subscribers -
</span>
<router-link
v-if="handle !== null"
class="handle"
:to="`/channel/${id}`"
>
{{ handle }}
</router-link>
<span
v-else
class="videoCount"
>
{{ videoCount }} videos
Expand Down
16 changes: 15 additions & 1 deletion src/renderer/components/ft-list-playlist/ft-list-playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export default Vue.extend({
}
},
mounted: function () {
if (typeof (this.data.owner) === 'object') {
// temporary until we've migrated the whole local API to youtubei.js
if (this.data.dataSource === 'local') {
this.parseLocalDataNew()
} else if (typeof (this.data.owner) === 'object') {
this.parseLocalData()
} else {
this.parseInvidiousData()
Expand Down Expand Up @@ -98,6 +101,17 @@ export default Vue.extend({
this.videoCount = this.data.length
},

// TODO: after the local API is fully switched to YouTube.js
// cleanup the old local API stuff
parseLocalDataNew: function () {
this.title = this.data.title
this.thumbnail = this.data.thumbnail
this.channelName = this.data.channelName
this.channelLink = this.data.channelId
this.playlistLink = this.data.playlistId
this.videoCount = this.data.videoCount
},

...mapActions([
'openInExternalPlayer'
])
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,9 @@ export default Vue.extend({
this.isPremium = this.data.premium || false
this.viewCount = this.data.viewCount

if (typeof (this.data.premiereTimestamp) !== 'undefined') {
if (typeof this.data.premiereDate !== 'undefined') {
this.publishedText = this.data.premiereDate.toLocaleString()
} else if (typeof (this.data.premiereTimestamp) !== 'undefined') {
this.publishedText = new Date(this.data.premiereTimestamp * 1000).toLocaleString()
} else {
this.publishedText = this.data.publishedText
Expand Down
7 changes: 5 additions & 2 deletions src/renderer/components/ft-list-video/ft-list-video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
<div
v-if="isLive || duration !== '0:00'"
class="videoDuration"
:class="{ live: isLive }"
:class="{
live: isLive,
upcoming: isUpcoming
}"
>
{{ isLive ? $t("Video.Live") : duration }}
{{ isLive ? $t("Video.Live") : (isUpcoming ? $t("Video.Upcoming") : duration) }}
</div>
<ft-icon-button
v-if="externalPlayer !== ''"
Expand Down
148 changes: 144 additions & 4 deletions src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { extractNumberFromString, getUserDataPath } from '../utils'
* @param {object} options
* @param {boolean} options.withPlayer set to true to get an Innertube instance that can decode the streaming URLs
* @param {string|undefined} options.location the geolocation to pass to YouTube get different content
* @param {boolean} options.safetyMode whether to hide mature content
* @returns the Innertube instance
*/
async function createInnertube(options = { withPlayer: false, location: undefined }) {
async function createInnertube(options = { withPlayer: false, location: undefined, safetyMode: false }) {
let cache
if (options.withPlayer) {
const userData = await getUserDataPath()
Expand All @@ -27,6 +28,7 @@ async function createInnertube(options = { withPlayer: false, location: undefine
return await Innertube.create({
retrieve_player: !!options.withPlayer,
location: options.location,
enable_safety_mode: !!options.safetyMode,
// use browser fetch
fetch: (input, init) => fetch(input, init),
cache
Expand Down Expand Up @@ -74,14 +76,62 @@ export async function getLocalTrending(location, tab, instance) {

const results = resultsInstance.videos
.filter((video) => video.type === 'Video')
.map(parseLocalListVideo)
.map(parseListVideo)

return {
results,
instance: resultsInstance
}
}

/**
* @param {string} query
* @param {object} filters
* @param {boolean} safetyMode
*/
export async function getLocalSearchResults(query, filters, safetyMode) {
const innertube = await createInnertube({ safetyMode })
const response = await innertube.search(query, convertSearchFilters(filters))

return handleSearchResponse(response)
}

/**
* @typedef {import('youtubei.js/dist/src/parser/youtube/Search').default} Search
*/

/**
* @param {Search} continuationData
*/
export async function getLocalSearchContinuation(continuationData) {
const response = await continuationData.getContinuation()

return handleSearchResponse(response)
}

/**
* @param {Search} response
*/
function handleSearchResponse(response) {
if (!response.results) {
return {
results: [],
continuationData: null
}
}

const results = response.results
.filter((item) => {
return item.type === 'Video' || item.type === 'Channel' || item.type === 'Playlist'
})
.map((item) => parseListItem(item))

return {
results,
continuationData: response.has_continuation ? response : null
}
}

/**
* @typedef {import('youtubei.js/dist/src/parser/classes/PlaylistVideo').default} PlaylistVideo
*/
Expand All @@ -106,7 +156,7 @@ export function parseLocalPlaylistVideo(video) {
/**
* @param {Video} video
*/
function parseLocalListVideo(video) {
function parseListVideo(video) {
return {
type: 'video',
videoId: video.id,
Expand All @@ -117,6 +167,96 @@ function parseLocalListVideo(video) {
viewCount: extractNumberFromString(video.view_count.text),
publishedText: video.published.text,
lengthSeconds: isNaN(video.duration.seconds) ? '' : video.duration.seconds,
liveNow: video.is_live
liveNow: video.is_live,
isUpcoming: video.is_upcoming || video.is_premiere,
premiereDate: video.upcoming
}
}

/**
* @typedef {import('youtubei.js/dist/src/parser/helpers').YTNode} YTNode
* @typedef {import('youtubei.js/dist/src/parser/classes/Channel').default} Channel
* @typedef {import('youtubei.js/dist/src/parser/classes/Playlist').default} Playlist
*/

/**
* @param {YTNode} item
*/
function parseListItem(item) {
switch (item.type) {
case 'Video':
return parseListVideo(item)
case 'Channel': {
/** @type {Channel} */
const channel = item

// see upstream TODO: https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes/Channel.ts#L33

// according to https://github.com/iv-org/invidious/issues/3514#issuecomment-1368080392
// the response can be the new or old one, so we currently need to handle both here
let subscribers
let videos = null
let handle = null
if (channel.subscribers.text.startsWith('@')) {
subscribers = channel.videos.text
handle = channel.subscribers.text
} else {
subscribers = channel.subscribers.text
videos = channel.videos.text
}

return {
type: 'channel',
dataSource: 'local',
thumbnail: channel.author.best_thumbnail?.url,
name: channel.author.name,
channelID: channel.author.id,
subscribers,
videos,
handle,
descriptionShort: channel.description_snippet.text
}
}
case 'Playlist': {
/** @type {Playlist} */
const playlist = item
return {
type: 'playlist',
dataSource: 'local',
title: playlist.title,
thumbnail: playlist.thumbnails[0].url,
channelName: playlist.author.name,
channelId: playlist.author.id,
playlistId: playlist.id,
videoCount: extractNumberFromString(playlist.video_count.text)
}
}
}
}

function convertSearchFilters(filters) {
const convertedFilters = {}

// some of the fields have different names and
// others have empty strings that we don't want to pass to youtubei.js

if (filters) {
if (filters.sortBy) {
convertedFilters.sort_by = filters.sortBy
}

if (filters.time) {
convertedFilters.upload_date = filters.time
}

if (filters.type) {
convertedFilters.type = filters.type
}

if (filters.duration) {
convertedFilters.type = filters.duration
}
}

return convertedFilters
}
2 changes: 1 addition & 1 deletion src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ export async function getPicturesPath() {

export function extractNumberFromString(str) {
if (typeof str === 'string') {
return parseInt(str.replace(/\D+/, ''))
return parseInt(str.replaceAll(/\D+/g, ''))
} else {
return NaN
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/scss-partials/_ft-list-item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ $watched-transition-duration: 0.5s;
}

.videoWatched,
.live {
.live,
.upcoming {
text-transform: uppercase;
}