Skip to content

Commit

Permalink
Migrate search to YouTube.js
Browse files Browse the repository at this point in the history
  • Loading branch information
absidue committed Jan 3, 2023
1 parent bc44e27 commit ee3e920
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 265 deletions.
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}/**/*',

'!**/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 @@ -76,8 +76,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
4 changes: 4 additions & 0 deletions src/renderer/components/ft-list-channel/ft-list-channel.sass
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
@use "../../sass-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.replace(/\D+/g, ''))
} else {
return NaN
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/sass-partials/_ft-list-item.sass
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,5 @@ $watched-transition-duration: 0.5s
margin-top: 8px
font-size: 13px

.videoWatched, .live
.videoWatched, .live, .upcoming
text-transform: uppercase

0 comments on commit ee3e920

Please sign in to comment.