I can't believe I figured out how to emulate ytmusic api calls

This commit is contained in:
Eclypsed
2024-02-24 02:06:02 -05:00
parent c7b9b214b7
commit fe37c8aa6e
12 changed files with 166 additions and 72 deletions

Binary file not shown.

View File

@@ -13,7 +13,7 @@ const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections(
userId VARCHAR(36) NOT NULL,
type VARCHAR(36) NOT NULL,
service TEXT,
tokens TEXT
tokens TEXT,
FOREIGN KEY(userId) REFERENCES Users(id)
)`
db.exec(initUsersTable), db.exec(initConnectionsTable)
@@ -77,11 +77,11 @@ export class Connections {
return connections
}
static addConnection<T extends serviceType>(type: T, connectionData: Omit<DBConnectionData<T>, 'id' | 'type'>): DBConnectionData<T> {
static addConnection<T extends serviceType>(type: T, connectionData: Omit<DBConnectionData<T>, 'id' | 'type'>): string {
const connectionId = generateUUID()
const { userId, service, tokens } = connectionData
db.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, service, tokens)
return this.getConnection(connectionId) as DBConnectionData<T>
db.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, JSON.stringify(service), JSON.stringify(tokens))
return connectionId
}
static deleteConnection = (id: string): void => {
@@ -91,7 +91,7 @@ export class Connections {
static updateTokens = (id: string, accessToken?: string, refreshToken?: string, expiry?: number): void => {
const newTokens = { accessToken, refreshToken, expiry }
const commandInfo = db.prepare(`UPDATE Connections SET tokens = ? WHERE id = ?`).run(newTokens, id)
const commandInfo = db.prepare(`UPDATE Connections SET tokens = ? WHERE id = ?`).run(JSON.stringify(newTokens), id)
if (commandInfo.changes === 0) throw new Error('Failed to update tokens')
}

14
src/lib/services.json Normal file
View File

@@ -0,0 +1,14 @@
{
"jellyfin": {
"displayName": "Jellyfin",
"type": ["streaming"],
"icon": "https://raw.githubusercontent.com/jellyfin/jellyfin-ux/55616553b692b1a6c7d8e786eeb7d8216e9b50df/branding/SVG/icon-transparent.svg",
"primaryColor": "--jellyfin-blue"
},
"youtube-music": {
"displayName": "YouTube Music",
"type": ["streaming"],
"icon": "https://upload.wikimedia.org/wikipedia/commons/6/6a/Youtube_Music_icon.svg",
"primaryColor": "--youtube-red"
}
}

View File

@@ -1,20 +1,5 @@
import { google } from 'googleapis'
export const serviceData = {
jellyfin: {
displayName: 'Jellyfin',
type: ['streaming'],
icon: 'https://raw.githubusercontent.com/jellyfin/jellyfin-ux/55616553b692b1a6c7d8e786eeb7d8216e9b50df/branding/SVG/icon-transparent.svg',
primaryColor: '--jellyfin-blue',
},
'youtube-music': {
displayName: 'YouTube Music',
type: ['streaming'],
icon: 'https://upload.wikimedia.org/wikipedia/commons/6/6a/Youtube_Music_icon.svg',
primaryColor: '--youtube-red',
},
}
export class Jellyfin {
static audioPresets = (userId: string) => {
return {
@@ -140,6 +125,16 @@ export class Jellyfin {
}
export class YouTubeMusic {
static baseHeaders = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0',
accept: '*/*',
'accept-encoding': 'gzip, deflate',
'content-type': 'application/json',
'content-encoding': 'gzip',
origin: 'https://music.youtube.com',
Cookie: 'SOCS=CAI;',
}
static fetchServiceInfo = async (userId: string, accessToken: string): Promise<Connection<'youtube-music'>['service']> => {
const youtube = google.youtube('v3')
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['snippet'], access_token: accessToken })
@@ -151,4 +146,58 @@ export class YouTubeMusic {
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined,
}
}
static getVisitorId = async (accessToken: string): Promise<string> => {
const headers = Object.assign(this.baseHeaders, { authorization: `Bearer ${accessToken}`, 'X-Goog-Request-Time': `${Date.now()}` })
const visitorIdResponse = await fetch('https://music.youtube.com', { headers })
const visitorIdText = await visitorIdResponse.text()
const regex = /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/g
const matches = []
let match
while ((match = regex.exec(visitorIdText)) !== null) {
const capturedGroup = match[1]
matches.push(capturedGroup)
}
let visitorId = ''
if (matches.length > 0) {
const ytcfg = JSON.parse(matches[0])
visitorId = ytcfg.VISITOR_DATA
}
return visitorId
}
static getHome = async (accessToken: string) => {
const headers = Object.assign(this.baseHeaders, { authorization: `Bearer ${accessToken}`, 'X-Goog-Request-Time': `${Date.now()}` })
function formatDate(): string {
const currentDate = new Date()
const year = currentDate.getUTCFullYear()
const month = (currentDate.getUTCMonth() + 1).toString().padStart(2, '0') // Months are zero-based, so add 1
const day = currentDate.getUTCDate().toString().padStart(2, '0')
return year + month + day
}
const response = await fetch(`https://music.youtube.com/youtubei/v1/browse?alt=json`, {
headers,
method: 'POST',
body: JSON.stringify({
browseId: 'FEmusic_home',
context: {
client: {
clientName: 'WEB_REMIX',
clientVersion: '1.' + formatDate() + '.01.00',
hl: 'en',
},
},
}),
})
const data = await response.json()
console.log(response.status)
console.log(data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].musicCarouselShelfRenderer.contents[0].musicTwoRowItemRenderer.title)
}
}