|
6 | 6 | */ |
7 | 7 |
|
8 | 8 | import { enhanceYTMusicArtwork } from '../Utils/ArtworkEnhancer'; |
| 9 | +import { getCachedData, CACHE_GROUPS } from './CacheManager'; |
| 10 | + |
| 11 | +// Cache constants for home feed |
| 12 | +const HOME_FEED_CACHE_KEY = 'ytmusic_home_sections_unified'; |
| 13 | +const HOME_FEED_CACHE_TTL_MINUTES = 1440; // 24 hours |
9 | 14 |
|
10 | 15 | const INNERTUBE_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; |
11 | 16 | const INNERTUBE_API_URL = 'https://music.youtube.com/youtubei/v1'; |
@@ -192,145 +197,186 @@ class InnerTubeClient { |
192 | 197 | return null; |
193 | 198 | } |
194 | 199 | } |
195 | | - /** |
196 | | - * Get Home Feed with continuation support |
197 | | - * Fetches sections by following continuation tokens and chips (like OuterTune) |
198 | | - * @param {number} sectionLimit - Maximum number of sections to fetch |
199 | | - */ |
200 | | - static async getHome(sectionLimit = 20) { |
201 | | - let authCookies = null; |
| 200 | +/** |
| 201 | + * Get Home Feed - cache-aware wrapper |
| 202 | + * Checks cache first, fetches fresh if cache miss or forceRefresh |
| 203 | + * @param {number} sectionLimit - Maximum number of sections to fetch |
| 204 | + * @param {boolean} forceRefresh - Skip cache if true |
| 205 | + * @returns {Promise<Array>} Array of home feed sections |
| 206 | + */ |
| 207 | +static async getHome(sectionLimit = 20, forceRefresh = false) { |
| 208 | + if (forceRefresh) { |
| 209 | + // Skip cache on force refresh |
| 210 | + return await InnerTubeClient.getHomeWithContinuation(sectionLimit); |
| 211 | + } |
202 | 212 |
|
203 | | - // Try to get auth cookies for personalized content |
204 | | - try { |
205 | | - const ytAuthService = require('../Utils/YouTubeAuthService').default; |
206 | | - if (ytAuthService.isAuth()) { |
207 | | - authCookies = await ytAuthService.getCookies(); |
208 | | - } |
209 | | - } catch (e) {} |
| 213 | + try { |
| 214 | + const cachedResult = await getCachedData( |
| 215 | + HOME_FEED_CACHE_KEY, |
| 216 | + async () => { |
| 217 | + // Fetch fresh data and wrap in success object for getCachedData |
| 218 | + const data = await InnerTubeClient.getHomeWithContinuation(sectionLimit); |
| 219 | + return { sections: data, success: true }; |
| 220 | + }, |
| 221 | + HOME_FEED_CACHE_TTL_MINUTES, |
| 222 | + CACHE_GROUPS.HOME, |
| 223 | + false // forceRefresh handled above |
| 224 | + ); |
210 | 225 |
|
211 | | - // Get user's language and country preference from settings |
212 | | - // Note: Language affects UI text, songs are based on listening HISTORY (visitorData) |
213 | | - // Use an account with listening history for personalized recommendations |
214 | | - let userLanguage = 'SYSTEM_DEFAULT'; |
215 | | - let userCountry = 'SYSTEM_DEFAULT'; |
216 | | - try { |
217 | | - const AsyncStorage = |
218 | | - require('@react-native-async-storage/async-storage').default; |
219 | | - const storedLang = await AsyncStorage.getItem('ytmusic_language'); |
220 | | - const storedCountry = await AsyncStorage.getItem('ytmusic_country'); |
221 | | - if (storedLang) { |
222 | | - userLanguage = storedLang; |
223 | | - } |
224 | | - if (storedCountry) { |
225 | | - userCountry = storedCountry; |
226 | | - } |
227 | | - } catch (e) {} |
| 226 | + // getCachedData returns the data from fetchFunction directly on cache miss |
| 227 | + // On cache hit, it returns the cached data |
| 228 | + if (cachedResult && Array.isArray(cachedResult)) { |
| 229 | + return cachedResult; |
| 230 | + } |
| 231 | + if (cachedResult?.sections && Array.isArray(cachedResult.sections)) { |
| 232 | + return cachedResult.sections; |
| 233 | + } |
| 234 | + // Fallback to fresh fetch |
| 235 | + return await InnerTubeClient.getHomeWithContinuation(sectionLimit); |
| 236 | + } catch (error) { |
| 237 | + console.warn('YTMusic home cache error, fetching fresh:', error.message); |
| 238 | + return await InnerTubeClient.getHomeWithContinuation(sectionLimit); |
| 239 | + } |
| 240 | +} |
228 | 241 |
|
229 | | - // Initial request with user's language preference |
230 | | - const data = await this.request( |
| 242 | +/** |
| 243 | + * Internal method - does the actual API fetching with continuation |
| 244 | + * @param {number} sectionLimit - Maximum number of sections to fetch |
| 245 | + */ |
| 246 | +static async getHomeWithContinuation(sectionLimit = 20) { |
| 247 | + let authCookies = null; |
| 248 | + |
| 249 | + // Try to get auth cookies for personalized content |
| 250 | + try { |
| 251 | + const ytAuthService = require('../Utils/YouTubeAuthService').default; |
| 252 | + if (ytAuthService.isAuth()) { |
| 253 | + authCookies = await ytAuthService.getCookies(); |
| 254 | + } |
| 255 | + } catch (e) {} |
| 256 | + |
| 257 | + // Get user's language and country preference from settings |
| 258 | + // Note: Language affects UI text, songs are based on listening HISTORY (visitorData) |
| 259 | + // Use an account with listening history for personalized recommendations |
| 260 | + let userLanguage = 'SYSTEM_DEFAULT'; |
| 261 | + let userCountry = 'SYSTEM_DEFAULT'; |
| 262 | + try { |
| 263 | + const AsyncStorage = |
| 264 | + require('@react-native-async-storage/async-storage').default; |
| 265 | + const storedLang = await AsyncStorage.getItem('ytmusic_language'); |
| 266 | + const storedCountry = await AsyncStorage.getItem('ytmusic_country'); |
| 267 | + if (storedLang) { |
| 268 | + userLanguage = storedLang; |
| 269 | + } |
| 270 | + if (storedCountry) { |
| 271 | + userCountry = storedCountry; |
| 272 | + } |
| 273 | + } catch (e) {} |
| 274 | + |
| 275 | + // Initial request with user's language preference |
| 276 | + const data = await this.request( |
| 277 | + 'browse', |
| 278 | + { browseId: 'FEmusic_home' }, |
| 279 | + userCountry, |
| 280 | + authCookies, |
| 281 | + userLanguage |
| 282 | + ); |
| 283 | + |
| 284 | + // Parse initial sections, chips, and continuation token |
| 285 | + let { sections, chips, continuation } = |
| 286 | + this.parseHomeWithContinuation(data); |
| 287 | + let allSections = [...sections]; |
| 288 | + const seenTitles = new Set(sections.map((s) => s.title)); |
| 289 | + |
| 290 | + // 1. Follow continuations iteratively (Main Home Feed) |
| 291 | + let continuationCount = 0; |
| 292 | + const MAX_CONTINUATIONS = 5; |
| 293 | + |
| 294 | + while ( |
| 295 | + continuation && |
| 296 | + allSections.length < sectionLimit && |
| 297 | + continuationCount < MAX_CONTINUATIONS |
| 298 | + ) { |
| 299 | + const contData = await this.request( |
231 | 300 | 'browse', |
232 | | - { browseId: 'FEmusic_home' }, |
| 301 | + { continuation }, |
233 | 302 | userCountry, |
234 | 303 | authCookies, |
235 | 304 | userLanguage |
236 | 305 | ); |
| 306 | + const contResult = this.parseHomeContinuation(contData); |
237 | 307 |
|
238 | | - // Parse initial sections, chips, and continuation token |
239 | | - let { sections, chips, continuation } = |
240 | | - this.parseHomeWithContinuation(data); |
241 | | - let allSections = [...sections]; |
242 | | - const seenTitles = new Set(sections.map((s) => s.title)); |
243 | | - |
244 | | - // 1. Follow continuations iteratively (Main Home Feed) |
245 | | - let continuationCount = 0; |
246 | | - const MAX_CONTINUATIONS = 5; |
247 | | - |
248 | | - while ( |
249 | | - continuation && |
250 | | - allSections.length < sectionLimit && |
251 | | - continuationCount < MAX_CONTINUATIONS |
252 | | - ) { |
253 | | - const contData = await this.request( |
254 | | - 'browse', |
255 | | - { continuation }, |
256 | | - userCountry, |
257 | | - authCookies, |
258 | | - userLanguage |
259 | | - ); |
260 | | - const contResult = this.parseHomeContinuation(contData); |
| 308 | + let addedInThisCont = 0; |
| 309 | + contResult.sections.forEach((section) => { |
| 310 | + if (section.title && !seenTitles.has(section.title)) { |
| 311 | + seenTitles.add(section.title); |
| 312 | + allSections.push(section); |
| 313 | + addedInThisCont++; |
| 314 | + } |
| 315 | + }); |
| 316 | + continuation = contResult.continuation; |
| 317 | + continuationCount++; |
261 | 318 |
|
262 | | - let addedInThisCont = 0; |
263 | | - contResult.sections.forEach((section) => { |
264 | | - if (section.title && !seenTitles.has(section.title)) { |
265 | | - seenTitles.add(section.title); |
266 | | - allSections.push(section); |
267 | | - addedInThisCont++; |
268 | | - } |
269 | | - }); |
270 | | - continuation = contResult.continuation; |
271 | | - continuationCount++; |
| 319 | + if (addedInThisCont === 0) { |
| 320 | + break; |
| 321 | + } // Stop if no new sections found |
| 322 | + } |
| 323 | + |
| 324 | + // 2. Fetch from chips (additional variety like OuterTune) |
| 325 | + if (chips && chips.length > 0 && allSections.length < sectionLimit) { |
| 326 | + const chipsToFetch = []; |
272 | 327 |
|
273 | | - if (addedInThisCont === 0) { |
274 | | - break; |
275 | | - } // Stop if no new sections found |
| 328 | + // Prioritize the "Music" chip if found (contains personalized "Albums for you") |
| 329 | + const musicChip = chips.find((c) => |
| 330 | + c.title.toLowerCase().includes('music') |
| 331 | + ); |
| 332 | + if (musicChip) { |
| 333 | + chipsToFetch.push(musicChip); |
276 | 334 | } |
277 | 335 |
|
278 | | - // 2. Fetch from chips (additional variety like OuterTune) |
279 | | - if (chips && chips.length > 0 && allSections.length < sectionLimit) { |
280 | | - const chipsToFetch = []; |
| 336 | + // Add other chips up to limit |
| 337 | + chips.forEach((c) => { |
| 338 | + if (c !== musicChip && chipsToFetch.length < 8) { |
| 339 | + chipsToFetch.push(c); |
| 340 | + } |
| 341 | + }); |
281 | 342 |
|
282 | | - // Prioritize the "Music" chip if found (contains personalized "Albums for you") |
283 | | - const musicChip = chips.find((c) => |
284 | | - c.title.toLowerCase().includes('music') |
285 | | - ); |
286 | | - if (musicChip) { |
287 | | - chipsToFetch.push(musicChip); |
| 343 | + const chipPromises = chipsToFetch.map(async (chip, idx) => { |
| 344 | + if (!chip.params) { |
| 345 | + return []; |
288 | 346 | } |
289 | 347 |
|
290 | | - // Add other chips up to limit |
291 | | - chips.forEach((c) => { |
292 | | - if (c !== musicChip && chipsToFetch.length < 8) { |
293 | | - chipsToFetch.push(c); |
294 | | - } |
295 | | - }); |
| 348 | + try { |
| 349 | + const chipData = await this.request( |
| 350 | + 'browse', |
| 351 | + { |
| 352 | + browseId: 'FEmusic_home', |
| 353 | + params: chip.params, |
| 354 | + }, |
| 355 | + userCountry, |
| 356 | + authCookies, |
| 357 | + userLanguage |
| 358 | + ); |
296 | 359 |
|
297 | | - const chipPromises = chipsToFetch.map(async (chip, idx) => { |
298 | | - if (!chip.params) { |
299 | | - return []; |
300 | | - } |
| 360 | + const chipResult = this.parseHomeWithContinuation(chipData); |
| 361 | + return chipResult.sections; |
| 362 | + } catch (e) { |
| 363 | + return []; |
| 364 | + } |
| 365 | + }); |
301 | 366 |
|
302 | | - try { |
303 | | - const chipData = await this.request( |
304 | | - 'browse', |
305 | | - { |
306 | | - browseId: 'FEmusic_home', |
307 | | - params: chip.params, |
308 | | - }, |
309 | | - userCountry, |
310 | | - authCookies, |
311 | | - userLanguage |
312 | | - ); |
| 367 | + const chipResultsArr = await Promise.all(chipPromises); |
313 | 368 |
|
314 | | - const chipResult = this.parseHomeWithContinuation(chipData); |
315 | | - return chipResult.sections; |
316 | | - } catch (e) { |
317 | | - return []; |
| 369 | + chipResultsArr.forEach((chipSections) => { |
| 370 | + chipSections.forEach((section) => { |
| 371 | + if (section.title && !seenTitles.has(section.title)) { |
| 372 | + seenTitles.add(section.title); |
| 373 | + allSections.push(section); |
318 | 374 | } |
319 | 375 | }); |
320 | | - |
321 | | - const chipResultsArr = await Promise.all(chipPromises); |
322 | | - |
323 | | - chipResultsArr.forEach((chipSections) => { |
324 | | - chipSections.forEach((section) => { |
325 | | - if (section.title && !seenTitles.has(section.title)) { |
326 | | - seenTitles.add(section.title); |
327 | | - allSections.push(section); |
328 | | - } |
329 | | - }); |
330 | | - }); |
331 | | - } |
332 | | - return allSections; |
| 376 | + }); |
333 | 377 | } |
| 378 | + return allSections; |
| 379 | +} |
334 | 380 |
|
335 | 381 | /** |
336 | 382 | * Get Search Results |
|
0 commit comments