File size: 3,420 Bytes
e24a52f
 
94f96fe
c22439c
4170f15
e24a52f
94f96fe
e24a52f
c22439c
94f96fe
e24a52f
 
 
7102b00
e24a52f
94f96fe
e24a52f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c22439c
 
 
 
 
94f96fe
 
20f1d9d
c22439c
 
6e879ea
c22439c
 
 
 
 
 
 
 
 
6e879ea
c22439c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e24a52f
 
c22439c
e24a52f
94f96fe
e24a52f
 
94f96fe
 
 
c22439c
 
 
 
 
7d6a166
 
 
 
 
 
94f96fe
78a24ef
c22439c
 
78a24ef
c22439c
 
 
78a24ef
c22439c
78a24ef
 
c22439c
 
 
 
 
 
4170f15
78a24ef
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { Browser, BrowserContext, Page } from 'playwright'
import { chromium } from 'playwright-extra'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
import { store } from './storage.js'
import { logMemory } from './utils.js'

chromium.use(StealthPlugin())

const createContext = async (browser: Browser) => {
   const context = await browser.newContext({
      locale: 'en-US',
      timezoneId: 'America/New_York',
      bypassCSP: true,
      deviceScaleFactor: 1,
   })
   await context.addCookies([
      {
         name: 'sp_dc',
         value: process.env.SP_DC!,
         domain: '.spotify.com',
         path: '/',
         httpOnly: true,
         secure: true,
         sameSite: 'None',
      },
      {
         name: 'sp_key',
         value: process.env.SP_KEY!,
         domain: '.spotify.com',
         path: '/',
         httpOnly: false,
         secure: true,
         sameSite: 'None',
      },
   ])
   return context
}

export const createPageFromBrowser = async (browser: Browser) => {
   const context = await createContext(browser)
   const page = await context.newPage()
   await page.route('**/*.{png,jpg,jpeg,gif,woff,woff2,sentry}', (r) => r.abort())
   await page.route('**/*{onetrust,i.scdn.co/image/,mosaic.scdn.co/,encore.scdn.co/fonts}*', (r) => r.abort())
   return page
}

let browserPromise: Promise<Browser> | null = null
export async function ensureBrowser(): Promise<Browser> {
   if (browserPromise) {
      const b = await browserPromise.catch(() => null)
      if (!b || !b.isConnected()) {
         browserPromise = null
         store.browser = null
      }
   }

   if (!browserPromise) {
      browserPromise = (async () => {
         const b = await chromium.launch({
            headless: true,
            args: ['--disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'],
         })
         b.on('disconnected', () => {
            console.warn('Browser disconnected')
            browserPromise = null
            store.browser = null
         })
         return b
      })()
   }

   const browser = await browserPromise
   store.browser = browser
   return browser
}

export async function restartBrowser() {
   if (store.browser) {
      await store.browser.close()
      store.browser = null
   }
   return ensureBrowser()
}

export function handleError(error: unknown) {
   const details = error instanceof Error ? error.message : 'Unknown error'
   console.error('💥 Error:', details)
   return details
}

export async function killBrowser(browser: Browser | null) {
   console.log(`<- Closing browser`)
   if (browser) await browser.close().catch(() => {})
}

export async function closeContexts(browser: Browser | null) {
   console.log(`<- Closing contexts`)
   if (browser) {
      const contexts = browser.contexts()
      for (const context of contexts) {
         await context.close().catch(() => {})
      }
   }
}

export async function withPage<T>(fn: (page: Page) => Promise<T>): Promise<T | null> {
   let page: Page | null = null
   try {
      const browser = await ensureBrowser()
      page = await createPageFromBrowser(browser)
      return await fn(page)
   } catch (err) {
      handleError(err)
      return null
   } finally {
      if (page) {
         await page
            .context()
            .close()
            .catch(() => {})
      }
      logMemory('Context closed')
   }
}