add account checking to lucida, bugfix soundcloud

This commit is contained in:
aria
2024-07-18 20:08:59 -04:00
parent 64fde66d82
commit f385032565
9 changed files with 123 additions and 33 deletions

View File

@@ -28,6 +28,28 @@ interface QobuzOptions {
token?: string
}
interface LoginResponse {
user: {
id: number
display_name: string
language_code: string
zone: string,
store: string
country: string
creation_date: string
credential: {
parameters: {
lossy_streaming: boolean
lossless_streaming: boolean
hires_streaming: boolean
}
label: string
source: string
}
}
user_auth_token: string
}
export default class Qobuz implements StreamerWithLogin {
hostnames = ['play.qobuz.com', 'open.qobuz.com', 'www.qobuz.com', 'qobuz.com']
token?: string
@@ -100,9 +122,6 @@ export default class Qobuz implements StreamerWithLogin {
app_id: this.appId
}
interface LoginResponse {
user_auth_token: string
}
const loginResponse = <LoginResponse>await this.#getSigned('user/login', params)
this.token = loginResponse.user_auth_token
}
@@ -148,7 +167,7 @@ export default class Qobuz implements StreamerWithLogin {
await this.#getSigned('track/getFileUrl', params)
)
if (trackFileResponse.sample == true)
throw new Error('Could not get non-sample file. Check your token.')
throw new Error(`Could not get non-sample file. Make sure the track isn't download-only.`)
const streamResponse = await fetch(trackFileResponse.url)
return {
mimeType: trackFileResponse.mime_type,
@@ -252,4 +271,14 @@ export default class Qobuz implements StreamerWithLogin {
throw new Error('URL unrecognised')
}
}
async getAccountInfo(): Promise<StreamerAccount> {
const loginResponse = <LoginResponse>await this.#getSigned('user/login', {extra: 'partner', device_manufacturer_id: 'undefined', app_id: this.appId})
return {
valid: true,
premium: loginResponse.user.credential.parameters.hires_streaming,
country: loginResponse.user.country,
explicit: true
}
}
}

View File

@@ -5,7 +5,9 @@ import {
Streamer,
SearchResults,
GetByUrlResponse,
GetStreamResponse
GetStreamResponse,
StreamerAccount,
TrackGetByUrlResponse
} from '../../types.js'
import {
parseAlbum,
@@ -118,15 +120,14 @@ export default class Soundcloud implements Streamer {
}
#formatURL(og: string, client: ScClient): string {
let url = og
const parsed = new URL(og)
if (!parsed.searchParams.get('user_id')) url = url + `&user_id=${client.anonId}`
if (!parsed.searchParams.get('client_id')) url = url + `&client_id=${client.id}`
if (!parsed.searchParams.get('app_version')) url = url + `&app_version=${client.version}`
if (!parsed.searchParams.get('app_locale')) url = url + `&app_locale=en`
if (client.anonId) parsed.searchParams.append('user_id', client.anonId)
if (client.id) parsed.searchParams.append('client_id', client.id)
if (client.version) parsed.searchParams.append('app_version', client.version)
if (!parsed.searchParams.get('app_locale')) parsed.searchParams.append('app_locale', 'en')
return url
return parsed.href
}
async #getMetadata(url: string): Promise<GetByUrlResponse> {
@@ -141,8 +142,9 @@ export default class Soundcloud implements Streamer {
switch (type) {
case 'track': {
const trackId = html
.split(`<link rel="alternate" href="android-app://com.soundcloud.android/soundcloud/sounds:`)?.[1]
.split(`"soundcloud://sounds:`)?.[1]
?.split(`">`)?.[0]
let naked = `https://api-v2.soundcloud.com/tracks/${trackId}`
let path = new URL(url).pathname
@@ -157,7 +159,7 @@ export default class Soundcloud implements Streamer {
).text()
)
return {
return <TrackGetByUrlResponse>{
type: 'track',
getStream: async (hq?: boolean) => {
if (!hq) hq = false
@@ -225,7 +227,7 @@ export default class Soundcloud implements Streamer {
async #getRawTrackInfo(id: number | string, client: ScClient) {
const api = JSON.parse(
await (
await fetch(this.#formatURL(`https://api-v2.soundcloud.com/tracks?ids=${id}`, client), {
await fetch(this.#formatURL(`hhttps://api-v2.soundcloud.com/tracks/${id}`, client), {
method: 'get',
headers: headers(this.oauthToken)
})
@@ -234,6 +236,15 @@ export default class Soundcloud implements Streamer {
return api
}
async getAccountInfo(): Promise<StreamerAccount> {
const track = <TrackGetByUrlResponse>await this.getByUrl('https://soundcloud.com/ween/polka-dot-tail')
const stream = await track.getStream()
if (stream.mimeType.startsWith('audio/mp4')) {
stream.stream.unpipe()
return {valid: true, premium: true, explicit: true}
}
else return {valid: true, premium: false, explicit: true}
}
}
async function fetchKey(response: string) {

View File

@@ -108,9 +108,17 @@ class Spotify implements StreamerWithLogin {
}
}
async getAccountInfo(): Promise<StreamerAccount> {
const info = await this.client.get.me()
let premium
if (info.plan == 'premium') premium = true
else premium = false
return {
valid: true,
premium: (await this.client.isPremium())
premium,
country: info.country,
explicit: info.allowExplicit
}
}
disconnect() {

View File

@@ -36,12 +36,13 @@ export function parseTrack(raw: SpotifyTrack) {
durationMs: raw.durationMs,
}
if (raw.album) track.album = parseAlbum(raw.album)
//if (raw?.isrc) track.isrc = raw.isrc
if (raw?.isrc) track.isrc = raw.isrc
if (raw?.album?.availableMarkets) track.regions = raw.album.availableMarkets
return track
}
export function parseAlbum(raw: SpotifyAlbum): Album {
return {
export function parseAlbum(raw: SpotifyAlbum) {
const album: Album = {
title: raw.name,
id: raw.id,
url: raw.externalUrl,
@@ -50,6 +51,10 @@ export function parseAlbum(raw: SpotifyAlbum): Album {
coverArtwork: parseThumbnails(raw.coverArtwork),
artists: raw.artists.map((e) => parseArtist(e)),
}
if (raw.availableMarkets) album.regions = raw.availableMarkets
return album
}
export function parseEpisode(raw: SpotifyEpisode) {

View File

@@ -1,2 +1,3 @@
export const TIDAL_AUTH_BASE = 'https://auth.tidal.com/v1/'
export const TIDAL_API_BASE = 'https://listen.tidal.com/v1/'
export const TIDAL_SUBSCRIPTION_BASE = 'https://api.tidal.com/v1/'

View File

@@ -6,9 +6,10 @@ import {
GetStreamResponse,
SearchResults,
Streamer,
Track
Track,
StreamerAccount
} from '../../types.js'
import { TIDAL_AUTH_BASE, TIDAL_API_BASE } from './constants.js'
import { TIDAL_AUTH_BASE, TIDAL_API_BASE, TIDAL_SUBSCRIPTION_BASE } from './constants.js'
import {
Contributor,
RawAlbum,
@@ -40,6 +41,22 @@ interface LoginData {
interface SessionData {
countryCode: string
userId: number
}
interface SubscriptionData {
startDate: string
validUntil: string
status: string
subscription: {
type: string
offlineGracePeriod: number
}
highestSoundQuality: string
premiumAccess: boolean
canGetTrial: boolean
paymentType: string,
paymentOverdue: boolean
}
export default class Tidal implements Streamer {
@@ -49,6 +66,7 @@ export default class Tidal implements Streamer {
refreshToken: string
expires: number
countryCode: string
userId: number | undefined
hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com']
failedAuth = false
constructor(options: TidalOptions) {
@@ -77,7 +95,7 @@ export default class Tidal implements Streamer {
'User-Agent': 'TIDAL_ANDROID/1039 okhttp/3.14.9'
}
}
async #get(url: string, params: { [key: string]: string | number } = {}): Promise<unknown> {
async #get(url: string, params: { [key: string]: string | number } = {}, base: string = TIDAL_API_BASE): Promise<unknown> {
if (this.failedAuth) throw new Error(`Last request failed to authorize, get new tokens`)
if (Date.now() > this.expires) await this.refresh()
if (!this.countryCode) await this.getCountryCode()
@@ -88,7 +106,7 @@ export default class Tidal implements Streamer {
if (typeof params[key] == 'number') params[key] = params[key].toString()
}
const response = await fetch(
`${TIDAL_API_BASE}${url}?${new URLSearchParams(<{ [key: string]: string }>params)}`,
`${base}${url}?${new URLSearchParams(<{ [key: string]: string }>params)}`,
{
headers: this.headers()
}
@@ -127,6 +145,7 @@ export default class Tidal implements Streamer {
if (sessionResponse.status != 200) return false
const sessionData = <SessionData>await sessionResponse.json()
this.countryCode = sessionData.countryCode
this.userId = sessionData.userId
return true
}
async getTokens() {
@@ -392,4 +411,15 @@ export default class Tidal implements Streamer {
}
}
}
async getAccountInfo(): Promise<StreamerAccount> {
if (!this.userId) await this.getCountryCode()
const subscription = <SubscriptionData>await this.#get(`users/${this.userId}/subscription`, {}, TIDAL_SUBSCRIPTION_BASE)
return {
valid: true,
premium: subscription.premiumAccess,
country: this.countryCode,
explicit: true
}
}
}

View File

@@ -32,6 +32,7 @@ export interface Album {
copyright?: string,
label?: string,
genre?: string[]
regions?: string[]
}
export interface Track {
@@ -52,9 +53,9 @@ export interface Track {
album?: Album
durationMs?: number
coverArtwork?: CoverArtwork[]
regions?: Region[]
genres?: string[]
releaseDate?: Date
regions?: string[]
description?: string
}
@@ -134,6 +135,7 @@ export interface StreamerAccount {
valid: boolean
premium?: boolean
country?: string
explicit?: boolean
}
export interface Streamer {
@@ -144,7 +146,7 @@ export interface Streamer {
| ((url: string) => Promise<GetByUrlResponse>)
| ((url: string, limit?: number) => Promise<GetByUrlResponse>)
disconnect?(): Promise<void>
getAccountInfo?(): Promise<StreamerAccount>
getAccountInfo(): Promise<StreamerAccount>
}
export interface StreamerWithLogin extends Streamer {