hitd2 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +86 -0
  2. package/api/HITApi.d.ts +27 -0
  3. package/api/HITApi.d.ts.map +1 -0
  4. package/api/HITApi.js +2 -0
  5. package/api/HITApi.js.map +7 -0
  6. package/core/config.d.ts +10 -0
  7. package/core/config.d.ts.map +1 -0
  8. package/core/config.js +2 -0
  9. package/core/config.js.map +7 -0
  10. package/core/filters.d.ts +4 -0
  11. package/core/filters.d.ts.map +1 -0
  12. package/core/filters.js +2 -0
  13. package/core/filters.js.map +7 -0
  14. package/core/format.d.ts +9 -0
  15. package/core/format.d.ts.map +1 -0
  16. package/core/format.js +2 -0
  17. package/core/format.js.map +7 -0
  18. package/core/http.d.ts +8 -0
  19. package/core/http.d.ts.map +1 -0
  20. package/core/http.js +2 -0
  21. package/core/http.js.map +7 -0
  22. package/core/index.d.ts +4 -0
  23. package/core/index.d.ts.map +1 -0
  24. package/core/index.js +2 -0
  25. package/core/index.js.map +7 -0
  26. package/core/normalize.d.ts +5 -0
  27. package/core/normalize.d.ts.map +1 -0
  28. package/core/normalize.js +2 -0
  29. package/core/normalize.js.map +7 -0
  30. package/core/resolver.d.ts +5 -0
  31. package/core/resolver.d.ts.map +1 -0
  32. package/core/resolver.js +2 -0
  33. package/core/resolver.js.map +7 -0
  34. package/core/retry.d.ts +46 -0
  35. package/core/retry.d.ts.map +1 -0
  36. package/core/retry.js +2 -0
  37. package/core/retry.js.map +7 -0
  38. package/core/scoring.d.ts +5 -0
  39. package/core/scoring.d.ts.map +1 -0
  40. package/core/scoring.js +2 -0
  41. package/core/scoring.js.map +7 -0
  42. package/core/types.d.ts +5 -0
  43. package/core/types.d.ts.map +1 -0
  44. package/core/types.js +2 -0
  45. package/core/types.js.map +7 -0
  46. package/index.d.ts +4 -0
  47. package/index.d.ts.map +1 -0
  48. package/index.js +2 -0
  49. package/index.js.map +7 -0
  50. package/models/audio.d.ts +14 -0
  51. package/models/audio.d.ts.map +1 -0
  52. package/models/audio.js +2 -0
  53. package/models/audio.js.map +7 -0
  54. package/models/track.d.ts +12 -0
  55. package/models/track.d.ts.map +1 -0
  56. package/models/track.js +2 -0
  57. package/models/track.js.map +7 -0
  58. package/package.json +30 -0
  59. package/providers/base.d.ts +11 -0
  60. package/providers/base.d.ts.map +1 -0
  61. package/providers/base.js +2 -0
  62. package/providers/base.js.map +7 -0
  63. package/providers/hitmos.provider.d.ts +13 -0
  64. package/providers/hitmos.provider.d.ts.map +1 -0
  65. package/providers/hitmos.provider.js +2 -0
  66. package/providers/hitmos.provider.js.map +7 -0
  67. package/providers/hitmoz.provider.d.ts +12 -0
  68. package/providers/hitmoz.provider.d.ts.map +1 -0
  69. package/providers/hitmoz.provider.js +2 -0
  70. package/providers/hitmoz.provider.js.map +7 -0
  71. package/providers/registry.d.ts +7 -0
  72. package/providers/registry.d.ts.map +1 -0
  73. package/providers/registry.js +2 -0
  74. package/providers/registry.js.map +7 -0
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # hitd2
2
+
3
+ > Unofficial Node.js wrapper for the Hitmo API ([hitmo.me](http://hitmo.me))
4
+
5
+ Search for music tracks, retrieve metadata, and extract MP3 audio URLs from hitmo sites.
6
+
7
+ ## Features
8
+
9
+ - Search tracks by query
10
+ - Get track metadata (title, artist, duration, cover image)
11
+ - Extract playable MP3 URLs
12
+ - Built-in retry with host failover (`hitmos.fm`, `hitmoz.org`)
13
+ - Caching via `hcacher` (configurable TTLs)
14
+ - TypeScript-first with full type definitions
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install hitd2
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { HITApi } from "hitd2";
26
+
27
+ const api = new HITApi();
28
+
29
+ const tracks = await api.search("Дико например");
30
+ console.log(tracks);
31
+
32
+ const track = await api.getTrack(tracks[0].id);
33
+ const audio = await api.getAudio(tracks[0].id);
34
+
35
+ console.log(track, audio);
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### `HITApi(options?)`
41
+
42
+ | Option | Type | Default | Description |
43
+ | --------------- | ------------- | ------- | ----------------------------------------- |
44
+ | `httpClient` | `HyperClient` | — | Custom HTTP client instance |
45
+ | `sessionCookie` | `string` | — | Session cookie for authenticated requests |
46
+
47
+ ### `search(query, limit?)`
48
+
49
+ Search for tracks. Returns `TrackMeta[]`.
50
+
51
+ ### `getTrack(trackId)`
52
+
53
+ Get full metadata for a track. Returns `TrackMeta`.
54
+
55
+ ### `getAudio(trackId)`
56
+
57
+ Extract best MP3 audio URL from track page. Returns `TrackAudio`.
58
+
59
+ ## Types
60
+
61
+ ```ts
62
+ interface TrackMeta {
63
+ id: string;
64
+ title: string;
65
+ artist: string;
66
+ duration: number; // seconds
67
+ uri: string; // "hitmos:track:{id}"
68
+ name: string;
69
+ duration_ms: number;
70
+ explicit: boolean;
71
+ image?: string;
72
+ }
73
+
74
+ interface TrackAudio {
75
+ id: string;
76
+ url: string; // best MP3 URL
77
+ format: string;
78
+ files: TrackAudioFile[];
79
+ urls: string[];
80
+ expiresAt: number;
81
+ }
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,27 @@
1
+ import { HyperClient } from "hyperttp";
2
+ import type { TrackMeta } from "../models/track.js";
3
+ import type { TrackAudio } from "../models/audio.js";
4
+ export declare class HITApi {
5
+ private readonly http;
6
+ private readonly retry;
7
+ private readonly providers;
8
+ private sessionCookie?;
9
+ private readonly trackCache;
10
+ private readonly searchCache;
11
+ private readonly audioCache;
12
+ constructor(options?: {
13
+ httpClient?: HyperClient;
14
+ sessionCookie?: string;
15
+ });
16
+ setSessionCookie(cookie: string): void;
17
+ private buildHeaders;
18
+ private fetchSearchHtml;
19
+ private providerFor;
20
+ private findTrackInSearchCache;
21
+ private searchCacheValues;
22
+ search(query: string, limit?: number): Promise<TrackMeta[]>;
23
+ getTrack(trackId: string): Promise<TrackMeta>;
24
+ getAudio(trackId: string): Promise<TrackAudio>;
25
+ }
26
+ export default HITApi;
27
+ //# sourceMappingURL=HITApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HITApi.d.ts","sourceRoot":"","sources":["../../src/api/HITApi.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAW,MAAM,UAAU,CAAC;AAWhD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQrD,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAE7C,OAAO,CAAC,aAAa,CAAC,CAAS;IAE/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkD;IAC7E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmD;IAC/E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkD;gBAEjE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,WAAW,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;IAU1E,gBAAgB,CAAC,MAAM,EAAE,MAAM;IAK/B,OAAO,CAAC,YAAY;YASN,eAAe;IAqB7B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAE,iBAAiB;IAUpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0G/D,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IA6D7C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CA4HrD;AAED,eAAe,MAAM,CAAC"}
package/api/HITApi.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";import{parse as E}from"node-html-parser";import{Request as v}from"hyperttp";import{CacheManager as C}from"hcacher";import{DEFAULT_HTTP_CONFIG as U}from"../core/config.js";import{Retry as H}from"../core/retry.js";import{normalizeQuery as O,normalizeTrackId as A,normalizeText as $,normalizeTitle as T}from"../core/normalize.js";import{sortTracksByScore as b}from"../core/scoring.js";import{HttpClient as M}from"../core/http.js";import{ProviderRegistry as R}from"../providers/registry.js";import{HitmosProvider as D}from"../providers/hitmos.provider.js";import{HitmozProvider as P}from"../providers/hitmoz.provider.js";import{isValidAudioUrl as g,scoreAudioUrl as S}from"../core/filters.js";export class HITApi{http;retry;providers;sessionCookie;trackCache=new C(1e3,15*6e4);searchCache=new C(200,10*6e4);audioCache=new C(500,50*6e4);constructor(r){this.http=new M(r?.httpClient),this.retry=new H({httpClient:r?.httpClient,sessionCookie:r?.sessionCookie}),this.sessionCookie=r?.sessionCookie,this.providers=new R([new D,new P])}setSessionCookie(r){this.sessionCookie=r,this.retry.setSessionCookie(r)}buildHeaders(r,e){return{"User-Agent":U.userAgent,Accept:e,Referer:`https://${r}/`,...this.sessionCookie?{Cookie:`sid=${this.sessionCookie}`}:{}}}async fetchSearchHtml(r,e){const o=new v({scheme:"https",host:r,port:443,path:"/search",query:{q:e},headers:this.buildHeaders(r,"text/html")});return this.retry.asText(await this.retry.withRetry(()=>this.http.getText(o),{attempts:3,retryOn:h=>{const d=String(h?.message??h);return/timeout|ECONNRESET|429|5\d\d|network|ENOTFOUND|ETIMEDOUT/i.test(d)}}))}providerFor(r,e){return this.providers.resolve(r,e)}findTrackInSearchCache(r){for(const e of this.searchCacheValues()){const o=e.find(h=>h.id===r);if(o)return o}}*searchCacheValues(){const r=this.searchCache.cache;if(r?.values)for(const e of r.values())Array.isArray(e)&&(yield e)}async search(r,e=20){const o=O(r),h=`${o}:${e}`,d=this.searchCache.get(h);if(d)return d;const a=await this.retry.resolveHost(),m=await this.fetchSearchHtml(a,r),c=E(m),i=this.providerFor(a,m).parseSearch(c).filter(t=>!(!t.id||!t.title?.trim()||!t.artist?.trim())).filter(t=>{const k=T(t.title);return!(["ringtone","\u0440\u0438\u043D\u0433\u0442\u043E\u043D","\u043C\u0438\u043D\u0443\u0441","remix","edit","nightcore","8d","bassboost","slowed","speed up"].some(w=>k.includes(w))||t.duration>0&&t.duration<45)}),f=new Map;for(const t of i)f.has(t.id)||f.set(t.id,t);const s=new Map;for(const t of f.values()){const k=[T(t.artist),T(t.title)].join(":"),y=s.get(k);if(!y){s.set(k,t);continue}const w=y.title.includes("(")?1:0;if((t.title.includes("(")?1:0)<w){s.set(k,t);continue}const x=Math.abs(y.duration-180);Math.abs(t.duration-180)<x&&s.set(k,t)}const p=b(o,[...s.values()]).slice(0,e);this.searchCache.set(h,p);for(const t of p)this.trackCache.set(t.id,t);return p}async getTrack(r){const e=A(r);return this.retry.dedupe(`meta:${e}`,async()=>{const o=this.trackCache.get(e),{html:h,root:d,host:a}=await this.retry.fetchTrackPage(e),m=this.providerFor(a,h);let c=o?.title??"Unknown",u=o?.artist??"Unknown",n=o?.duration??0,l=o?.image??"";if(c==="Unknown"||u==="Unknown"){const s=this.findTrackInSearchCache(e);s&&(c=s.title,u=s.artist,n=n||s.duration)}const i=m.parseTrackPage(d,a,e);if(i.title&&c==="Unknown"&&(c=i.title),i.artist&&u==="Unknown"&&(u=i.artist),i.duration&&!n&&(n=i.duration),i.image&&!l&&(l=i.image),c==="Unknown"||u==="Unknown"){const s=d.querySelector('meta[property="og:title"]')?.getAttribute("content")?.trim()||"";if(s){const p=$(s),t=this.retry.parseOgTitle(p);c==="Unknown"&&t.title!=="Unknown"&&(c=t.title),u==="Unknown"&&t.artist!=="Unknown"&&(u=t.artist)}}n||(n=this.retry.extractDuration(d)),l||(l=this.retry.extractImage(a,d));const f={id:e,title:c,artist:u,duration:n,uri:`hitmos:track:${e}`,name:c,duration_ms:n*1e3,explicit:!1,image:l};return this.trackCache.set(e,f),f})}async getAudio(r){const e=A(r),o=this.audioCache.get(e);return o||this.retry.dedupe(`audio:${e}`,async()=>{try{const{root:h,host:d}=await this.retry.fetchTrackPage(e);let a="";const m=[];for(const n of h.querySelectorAll("script")){const l=n.textContent||"",i=[...l.matchAll(/(https?:\/\/[^\s"'<>]+\.mp3[^\s"'<>]*)/g)];for(const p of i){const t=p[1].replace(/\\/g,"").trim();g(t)&&m.push(t)}const f=[...l.matchAll(/"url"\s*:\s*"([^"]+)"/g)];for(const p of f){const t=p[1].replace(/\\/g,"").trim();t.includes(".mp3")&&g(t)&&m.push(t)}const s=[...l.matchAll(/["'](\/L[a-zA-Z0-9_=]+\.mp3)["']/g)];for(const p of s){const t=`https://pl1.hitmos.fm${p[1]}`.replace(/\\/g,"").trim();g(t)&&m.push(t)}}const c=[...new Set(m)];if(c.sort((n,l)=>S(l)-S(n)),a=c[0]||"",!a)try{const n=new v({scheme:"https",host:d,port:443,path:`/api/track/${e}/play`,headers:{"User-Agent":U.userAgent,Accept:"application/json",Referer:`https://${d}/`,...this.sessionCookie&&{Cookie:`sid=${this.sessionCookie}`}}}),i=(await this.retry.withRetry(()=>this.http.getJson(n),{attempts:2,retryOn:f=>{const s=String(f?.message??f);return/timeout|ECONNRESET|429|5\d\d|network|ENOTFOUND|ETIMEDOUT/i.test(s)}}))?.url;i&&g(i)&&(a=String(i).replace(/\\/g,"").trim())}catch{}if(!a)throw new Error(`Failed to extract audio URL for track ${e}`);const u={id:e,url:a,format:"mp3",files:[{url:a,format:"mp3",bitrate:320}],urls:[a],expiresAt:Date.now()+36e5};return this.audioCache.set(e,u),u}catch(h){throw this.retry.invalidateHost(),h}})}}export default HITApi;
2
+ //# sourceMappingURL=HITApi.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/api/HITApi.ts"],
4
+ "sourcesContent": ["import { parse } from \"node-html-parser\";\nimport { HyperClient, Request } from \"hyperttp\";\nimport { CacheManager } from \"hcacher\";\nimport { DEFAULT_HTTP_CONFIG } from \"../core/config.js\";\nimport { Retry } from \"../core/retry.js\";\nimport {\n normalizeQuery,\n normalizeTrackId,\n normalizeText,\n normalizeTitle,\n} from \"../core/normalize.js\";\nimport { sortTracksByScore } from \"../core/scoring.js\";\nimport type { TrackMeta } from \"../models/track.js\";\nimport type { TrackAudio } from \"../models/audio.js\";\nimport { HttpClient } from \"../core/http.js\";\nimport { ProviderRegistry } from \"../providers/registry.js\";\nimport { HitmosProvider } from \"../providers/hitmos.provider.js\";\nimport { HitmozProvider } from \"../providers/hitmoz.provider.js\";\nimport type { SiteProvider } from \"../providers/base.js\";\nimport { isValidAudioUrl, scoreAudioUrl } from \"../core/filters.js\";\n\nexport class HITApi {\n private readonly http: HttpClient;\n private readonly retry: Retry;\n private readonly providers: ProviderRegistry;\n\n private sessionCookie?: string;\n\n private readonly trackCache = new CacheManager<TrackMeta>(1000, 15 * 60_000);\n private readonly searchCache = new CacheManager<TrackMeta[]>(200, 10 * 60_000);\n private readonly audioCache = new CacheManager<TrackAudio>(500, 50 * 60_000);\n\n constructor(options?: { httpClient?: HyperClient; sessionCookie?: string }) {\n this.http = new HttpClient(options?.httpClient);\n this.retry = new Retry({\n httpClient: options?.httpClient,\n sessionCookie: options?.sessionCookie,\n });\n this.sessionCookie = options?.sessionCookie;\n this.providers = new ProviderRegistry([new HitmosProvider(), new HitmozProvider()]);\n }\n\n setSessionCookie(cookie: string) {\n this.sessionCookie = cookie;\n this.retry.setSessionCookie(cookie);\n }\n\n private buildHeaders(host: string, accept: string): Record<string, string> {\n return {\n \"User-Agent\": DEFAULT_HTTP_CONFIG.userAgent,\n Accept: accept,\n Referer: `https://${host}/`,\n ...(this.sessionCookie ? { Cookie: `sid=${this.sessionCookie}` } : {}),\n };\n }\n\n private async fetchSearchHtml(host: string, query: string): Promise<string> {\n const req = new Request({\n scheme: \"https\",\n host,\n port: 443,\n path: \"/search\",\n query: { q: query },\n headers: this.buildHeaders(host, \"text/html\"),\n });\n\n return this.retry.asText(\n await this.retry.withRetry(() => this.http.getText(req), {\n attempts: 3,\n retryOn: (error) => {\n const msg = String((error as any)?.message ?? error);\n return /timeout|ECONNRESET|429|5\\d\\d|network|ENOTFOUND|ETIMEDOUT/i.test(msg);\n },\n }),\n );\n }\n\n private providerFor(host: string, html?: string): SiteProvider {\n return this.providers.resolve(host, html);\n }\n\n private findTrackInSearchCache(id: string): TrackMeta | undefined {\n for (const value of this.searchCacheValues()) {\n const found = value.find((t) => t.id === id);\n if (found) return found;\n }\n return undefined;\n }\n\n private *searchCacheValues(): Iterable<TrackMeta[]> {\n // small helper because Cache is opaque\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const inner = (this.searchCache as any).cache;\n if (!inner?.values) return;\n for (const value of inner.values()) {\n if (Array.isArray(value)) yield value as TrackMeta[];\n }\n }\n\n async search(query: string, limit: number = 20): Promise<TrackMeta[]> {\n const normalizedQuery = normalizeQuery(query);\n const cacheKey = `${normalizedQuery}:${limit}`;\n\n const cached = this.searchCache.get(cacheKey);\n if (cached) return cached;\n\n const host = await this.retry.resolveHost();\n\n const html = await this.fetchSearchHtml(host, query);\n\n const root = parse(html);\n\n const provider = this.providerFor(host, html);\n\n const raw = provider.parseSearch(root);\n\n // remove invalid\n const valid = raw.filter((track) => {\n if (!track.id) return false;\n if (!track.title?.trim()) return false;\n if (!track.artist?.trim()) return false;\n\n return true;\n });\n\n // remove garbage\n const filtered = valid.filter((track) => {\n const title = normalizeTitle(track.title);\n\n const blocked = [\n \"ringtone\",\n \"\u0440\u0438\u043D\u0433\u0442\u043E\u043D\",\n \"\u043C\u0438\u043D\u0443\u0441\",\n \"remix\",\n \"edit\",\n \"nightcore\",\n \"8d\",\n \"bassboost\",\n \"slowed\",\n \"speed up\",\n ];\n\n if (blocked.some((x) => title.includes(x))) {\n return false;\n }\n\n // skip tiny audio\n if (track.duration > 0 && track.duration < 45) {\n return false;\n }\n\n return true;\n });\n\n // dedupe by id\n const dedupedById = new Map<string, TrackMeta>();\n\n for (const track of filtered) {\n if (!dedupedById.has(track.id)) {\n dedupedById.set(track.id, track);\n }\n }\n\n // dedupe by normalized title+artist\n const canonical = new Map<string, TrackMeta>();\n\n for (const track of dedupedById.values()) {\n const key = [normalizeTitle(track.artist), normalizeTitle(track.title)].join(\":\");\n\n const existing = canonical.get(key);\n\n if (!existing) {\n canonical.set(key, track);\n continue;\n }\n\n // prefer canonical versions\n const existingPenalty = existing.title.includes(\"(\") ? 1 : 0;\n const currentPenalty = track.title.includes(\"(\") ? 1 : 0;\n\n if (currentPenalty < existingPenalty) {\n canonical.set(key, track);\n continue;\n }\n\n // prefer realistic duration\n const existingDurationScore = Math.abs(existing.duration - 180);\n const currentDurationScore = Math.abs(track.duration - 180);\n\n if (currentDurationScore < existingDurationScore) {\n canonical.set(key, track);\n }\n }\n\n const ranked = sortTracksByScore(normalizedQuery, [...canonical.values()]).slice(0, limit);\n\n this.searchCache.set(cacheKey, ranked);\n\n for (const track of ranked) {\n this.trackCache.set(track.id, track);\n }\n\n return ranked;\n }\n\n async getTrack(trackId: string): Promise<TrackMeta> {\n const id = normalizeTrackId(trackId);\n\n return this.retry.dedupe(`meta:${id}`, async () => {\n const cached = this.trackCache.get(id);\n const { html, root, host } = await this.retry.fetchTrackPage(id);\n const provider = this.providerFor(host, html);\n\n let title = cached?.title ?? \"Unknown\";\n let artist = cached?.artist ?? \"Unknown\";\n let duration = cached?.duration ?? 0;\n let image = cached?.image ?? \"\";\n\n if (title === \"Unknown\" || artist === \"Unknown\") {\n const searchHit = this.findTrackInSearchCache(id);\n if (searchHit) {\n title = searchHit.title;\n artist = searchHit.artist;\n duration = duration || searchHit.duration;\n }\n }\n\n const providerMeta = provider.parseTrackPage(root, host, id);\n if (providerMeta.title && title === \"Unknown\") title = providerMeta.title;\n if (providerMeta.artist && artist === \"Unknown\") artist = providerMeta.artist;\n if (providerMeta.duration && !duration) duration = providerMeta.duration;\n if (providerMeta.image && !image) image = providerMeta.image;\n\n if (title === \"Unknown\" || artist === \"Unknown\") {\n const ogTitle =\n root.querySelector('meta[property=\"og:title\"]')?.getAttribute(\"content\")?.trim() || \"\";\n\n if (ogTitle) {\n const cleaned = normalizeText(ogTitle);\n const parsed = this.retry.parseOgTitle(cleaned);\n\n if (title === \"Unknown\" && parsed.title !== \"Unknown\") title = parsed.title;\n if (artist === \"Unknown\" && parsed.artist !== \"Unknown\") artist = parsed.artist;\n }\n }\n\n if (!duration) duration = this.retry.extractDuration(root);\n if (!image) image = this.retry.extractImage(host, root);\n\n const track: TrackMeta = {\n id,\n title,\n artist,\n duration,\n uri: `hitmos:track:${id}`,\n name: title,\n duration_ms: duration * 1000,\n explicit: false,\n image,\n };\n\n this.trackCache.set(id, track);\n return track;\n });\n }\n\n async getAudio(trackId: string): Promise<TrackAudio> {\n const id = normalizeTrackId(trackId);\n\n const cached = this.audioCache.get(id);\n if (cached) return cached;\n\n return this.retry.dedupe(`audio:${id}`, async () => {\n try {\n const { root, host } = await this.retry.fetchTrackPage(id);\n\n let audioUrl = \"\";\n\n const candidates: string[] = [];\n\n for (const script of root.querySelectorAll(\"script\")) {\n const content = script.textContent || \"\";\n\n // \u0432\u0441\u0435 mp3 \u0441\u0441\u044B\u043B\u043A\u0438\n const directMatches = [...content.matchAll(/(https?:\\/\\/[^\\s\"'<>]+\\.mp3[^\\s\"'<>]*)/g)];\n\n for (const match of directMatches) {\n const candidate = match[1]!.replace(/\\\\/g, \"\").trim();\n\n if (isValidAudioUrl(candidate)) {\n candidates.push(candidate);\n }\n }\n\n // JSON url\n const jsonMatches = [...content.matchAll(/\"url\"\\s*:\\s*\"([^\"]+)\"/g)];\n\n for (const match of jsonMatches) {\n const candidate = match[1]!.replace(/\\\\/g, \"\").trim();\n\n if (candidate.includes(\".mp3\") && isValidAudioUrl(candidate)) {\n candidates.push(candidate);\n }\n }\n\n // base64 hitmos mp3\n const base64Matches = [...content.matchAll(/[\"'](\\/L[a-zA-Z0-9_=]+\\.mp3)[\"']/g)];\n\n for (const match of base64Matches) {\n const candidate = `https://pl1.hitmos.fm${match[1]}`.replace(/\\\\/g, \"\").trim();\n\n if (isValidAudioUrl(candidate)) {\n candidates.push(candidate);\n }\n }\n }\n\n // dedupe\n const uniqueCandidates = [...new Set(candidates)];\n\n // \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E score\n uniqueCandidates.sort((a, b) => scoreAudioUrl(b) - scoreAudioUrl(a));\n\n audioUrl = uniqueCandidates[0] || \"\";\n\n // fallback API\n if (!audioUrl) {\n try {\n const apiReq = new Request({\n scheme: \"https\",\n host,\n port: 443,\n path: `/api/track/${id}/play`,\n headers: {\n \"User-Agent\": DEFAULT_HTTP_CONFIG.userAgent,\n Accept: \"application/json\",\n Referer: `https://${host}/`,\n ...(this.sessionCookie && {\n Cookie: `sid=${this.sessionCookie}`,\n }),\n },\n });\n\n const res = await this.retry.withRetry(() => this.http.getJson(apiReq), {\n attempts: 2,\n retryOn: (error) => {\n const msg = String((error as any)?.message ?? error);\n\n return /timeout|ECONNRESET|429|5\\d\\d|network|ENOTFOUND|ETIMEDOUT/i.test(msg);\n },\n });\n\n const url = (res as any)?.url;\n\n if (url && isValidAudioUrl(url)) {\n audioUrl = String(url).replace(/\\\\/g, \"\").trim();\n }\n } catch {\n // ignore fallback errors\n }\n }\n\n if (!audioUrl) {\n throw new Error(`Failed to extract audio URL for track ${id}`);\n }\n\n const result: TrackAudio = {\n id,\n url: audioUrl,\n format: \"mp3\",\n files: [\n {\n url: audioUrl,\n format: \"mp3\",\n bitrate: 320,\n },\n ],\n urls: [audioUrl],\n expiresAt: Date.now() + 3_600_000,\n };\n\n this.audioCache.set(id, result);\n\n return result;\n } catch (error) {\n this.retry.invalidateHost();\n throw error;\n }\n });\n }\n}\n\nexport default HITApi;\n"],
5
+ "mappings": "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",
6
+ "names": ["parse", "Request", "CacheManager", "DEFAULT_HTTP_CONFIG", "Retry", "normalizeQuery", "normalizeTrackId", "normalizeText", "normalizeTitle", "sortTracksByScore", "HttpClient", "ProviderRegistry", "HitmosProvider", "HitmozProvider", "isValidAudioUrl", "scoreAudioUrl", "options", "cookie", "host", "accept", "query", "req", "error", "msg", "html", "id", "value", "found", "t", "inner", "limit", "normalizedQuery", "cacheKey", "cached", "root", "filtered", "track", "title", "x", "dedupedById", "canonical", "key", "existing", "existingPenalty", "existingDurationScore", "ranked", "trackId", "provider", "artist", "duration", "image", "searchHit", "providerMeta", "ogTitle", "cleaned", "parsed", "audioUrl", "candidates", "script", "content", "directMatches", "match", "candidate", "jsonMatches", "base64Matches", "uniqueCandidates", "a", "b", "apiReq", "url", "result"]
7
+ }
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_HTTP_CONFIG: {
2
+ timeout: number;
3
+ maxRetries: number;
4
+ userAgent: string;
5
+ enableCache: boolean;
6
+ verbose: boolean;
7
+ maxResponseBytes: number;
8
+ logger: (level: string, message: string, meta?: unknown) => void;
9
+ };
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB;;;;;;;oBAQd,MAAM,WAAW,MAAM,SAAS,OAAO;CAGxD,CAAC"}
package/core/config.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";export const DEFAULT_HTTP_CONFIG={timeout:15e3,maxRetries:2,userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",enableCache:!1,verbose:!0,maxResponseBytes:104857600,logger:(e,o,s)=>{console.log(`[HTTP ${e.toUpperCase()}] ${o}`,s??"")}};
2
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/config.ts"],
4
+ "sourcesContent": ["export const DEFAULT_HTTP_CONFIG = {\n timeout: 15_000,\n maxRetries: 2,\n userAgent:\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36\",\n enableCache: false,\n verbose: true,\n maxResponseBytes: 100 * 1024 * 1024,\n logger: (level: string, message: string, meta?: unknown) => {\n console.log(`[HTTP ${level.toUpperCase()}] ${message}`, meta ?? \"\");\n },\n};\n"],
5
+ "mappings": "aAAO,aAAM,oBAAsB,CACjC,QAAS,KACT,WAAY,EACZ,UACE,kHACF,YAAa,GACb,QAAS,GACT,iBAAkB,UAClB,OAAQ,CAACA,EAAeC,EAAiBC,IAAkB,CACzD,QAAQ,IAAI,SAASF,EAAM,YAAW,CAAE,KAAKC,CAAO,GAAIC,GAAQ,EAAE,CACpE",
6
+ "names": ["level", "message", "meta"]
7
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isBadTitle(title: string): boolean;
2
+ export declare function isValidAudioUrl(url: string): boolean;
3
+ export declare function scoreAudioUrl(url: string): number;
4
+ //# sourceMappingURL=filters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filters.d.ts","sourceRoot":"","sources":["../../src/core/filters.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAuBjD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAwBpD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiBjD"}
@@ -0,0 +1,2 @@
1
+ "use strict";export function isBadTitle(s){const i=s.toLowerCase(),e=[/популярн(ое|ый)\s*вк/i,/\bvk\b/i,/tiktok/i,/instagram/i,/8d/i,/bass\s*boost/i,/nightcore/i,/slowed/i,/sped\s*up/i,/lyrics|текст/i,/radio edit/i],r=[/\b(минус|instrumental|karaoke|cover|remix|mix|rington|рингтон)\b/i,/\(([^)]*(минус|remix|cover|instrumental|8d)[^)]*)\)/i];return e.some(t=>t.test(i))||r.some(t=>t.test(i))}export function isValidAudioUrl(s){const i=s.toLowerCase();return i.includes("radio")||i.includes("stream")||i.includes("hostingradio")||i.includes("listen")||i.includes("retro256")||i.includes("128.mp3")||i.includes("320.mp3")?!1:!!(i.includes("hitmos")||i.includes("track.mp3")||i.includes("/mp3/"))}export function scoreAudioUrl(s){const i=s.toLowerCase();let e=0;return i.includes("pl1.hitmos.fm")&&(e+=100),i.includes("pl2.hitmos.fm")&&(e+=90),i.includes("track.mp3")&&(e+=50),i.includes("/mp3/")&&(e+=25),i.includes("radio")&&(e-=1e3),i.includes("hostingradio")&&(e-=1e3),i.includes("stream")&&(e-=500),e}
2
+ //# sourceMappingURL=filters.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/filters.ts"],
4
+ "sourcesContent": ["export function isBadTitle(title: string): boolean {\n const t = title.toLowerCase();\n\n const hardNoise = [\n /\u043F\u043E\u043F\u0443\u043B\u044F\u0440\u043D(\u043E\u0435|\u044B\u0439)\\s*\u0432\u043A/i,\n /\\bvk\\b/i,\n /tiktok/i,\n /instagram/i,\n /8d/i,\n /bass\\s*boost/i,\n /nightcore/i,\n /slowed/i,\n /sped\\s*up/i,\n /lyrics|\u0442\u0435\u043A\u0441\u0442/i,\n /radio edit/i,\n ];\n\n const badVariants = [\n /\\b(\u043C\u0438\u043D\u0443\u0441|instrumental|karaoke|cover|remix|mix|rington|\u0440\u0438\u043D\u0433\u0442\u043E\u043D)\\b/i,\n /\\(([^)]*(\u043C\u0438\u043D\u0443\u0441|remix|cover|instrumental|8d)[^)]*)\\)/i,\n ];\n\n return hardNoise.some((r) => r.test(t)) || badVariants.some((r) => r.test(t));\n}\n\nexport function isValidAudioUrl(url: string): boolean {\n const lower = url.toLowerCase();\n\n // \u043C\u0443\u0441\u043E\u0440\u043D\u044B\u0435 \u0440\u0430\u0434\u0438\u043E/\u0441\u0442\u0440\u0438\u043C URL\n if (\n lower.includes(\"radio\") ||\n lower.includes(\"stream\") ||\n lower.includes(\"hostingradio\") ||\n lower.includes(\"listen\")\n ) {\n return false;\n }\n\n // \u043F\u043E\u0434\u043E\u0437\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 generic mp3\n if (lower.includes(\"retro256\") || lower.includes(\"128.mp3\") || lower.includes(\"320.mp3\")) {\n return false;\n }\n\n // \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0435 hitmos CDN\n if (lower.includes(\"hitmos\") || lower.includes(\"track.mp3\") || lower.includes(\"/mp3/\")) {\n return true;\n }\n\n return false;\n}\n\nexport function scoreAudioUrl(url: string): number {\n const lower = url.toLowerCase();\n\n let score = 0;\n\n // \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442 hitmos CDN\n if (lower.includes(\"pl1.hitmos.fm\")) score += 100;\n if (lower.includes(\"pl2.hitmos.fm\")) score += 90;\n if (lower.includes(\"track.mp3\")) score += 50;\n if (lower.includes(\"/mp3/\")) score += 25;\n\n // \u0448\u0442\u0440\u0430\u0444\u044B\n if (lower.includes(\"radio\")) score -= 1000;\n if (lower.includes(\"hostingradio\")) score -= 1000;\n if (lower.includes(\"stream\")) score -= 500;\n\n return score;\n}\n"],
5
+ "mappings": "aAAM,gBAAU,WAAWA,EAAa,CACtC,MAAMC,EAAID,EAAM,YAAW,EAErBE,EAAY,CAChB,wBACA,UACA,UACA,aACA,MACA,gBACA,aACA,UACA,aACA,gBACA,eAGIC,EAAc,CAClB,oEACA,wDAGF,OAAOD,EAAU,KAAME,GAAMA,EAAE,KAAKH,CAAC,CAAC,GAAKE,EAAY,KAAMC,GAAMA,EAAE,KAAKH,CAAC,CAAC,CAC9E,CAEM,gBAAU,gBAAgBI,EAAW,CACzC,MAAMC,EAAQD,EAAI,YAAW,EAa7B,OATEC,EAAM,SAAS,OAAO,GACtBA,EAAM,SAAS,QAAQ,GACvBA,EAAM,SAAS,cAAc,GAC7BA,EAAM,SAAS,QAAQ,GAMrBA,EAAM,SAAS,UAAU,GAAKA,EAAM,SAAS,SAAS,GAAKA,EAAM,SAAS,SAAS,EAC9E,GAIL,GAAAA,EAAM,SAAS,QAAQ,GAAKA,EAAM,SAAS,WAAW,GAAKA,EAAM,SAAS,OAAO,EAKvF,CAEM,gBAAU,cAAcD,EAAW,CACvC,MAAMC,EAAQD,EAAI,YAAW,EAE7B,IAAIE,EAAQ,EAGZ,OAAID,EAAM,SAAS,eAAe,IAAGC,GAAS,KAC1CD,EAAM,SAAS,eAAe,IAAGC,GAAS,IAC1CD,EAAM,SAAS,WAAW,IAAGC,GAAS,IACtCD,EAAM,SAAS,OAAO,IAAGC,GAAS,IAGlCD,EAAM,SAAS,OAAO,IAAGC,GAAS,KAClCD,EAAM,SAAS,cAAc,IAAGC,GAAS,KACzCD,EAAM,SAAS,QAAQ,IAAGC,GAAS,KAEhCA,CACT",
6
+ "names": ["title", "t", "hardNoise", "badVariants", "r", "url", "lower", "score"]
7
+ }
@@ -0,0 +1,9 @@
1
+ export declare class Format {
2
+ private retry;
3
+ constructor();
4
+ normalizeQuery(query: string): string;
5
+ isBadVariant(title: string): boolean;
6
+ private getTrackType;
7
+ scoreSearchCandidate(title: string, artist: string, query: string): number;
8
+ }
9
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/core/format.ts"],"names":[],"mappings":"AAEA,qBAAa,MAAM;IACjB,OAAO,CAAC,KAAK,CAAQ;;IAMrB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIrC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAWpC,OAAO,CAAC,YAAY;IAcpB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;CAgE3E"}
package/core/format.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";import{Retry as m}from"./retry.js";export class Format{retry;constructor(){this.retry=new m}normalizeQuery(s){return this.retry.normalizeText(s).toLowerCase()}isBadVariant(s){const e=s.toLowerCase();return[/\b(минус|instrumental|karaoke|караоке|remix|mix|bassboosted|rington|рингтон|cover|live|lyrics?|текст|version|ver\.?|slowed|sped\s*up|nightcore)\b/i,/\((?:[^)]*\b(?:минус|instrumental|karaoke|remix|mix|bassboosted|rington|рингтон|cover|live|lyrics?|текст|version|ver\.?|slowed|sped\s*up|nightcore)\b[^)]*)\)/i].some(r=>r.test(e))}getTrackType(s){const e=this.normalizeQuery(s);return!this.isBadVariant(e)&&!e.includes("(")?0:e.includes("2017")||e.includes("album")||e.includes("original")?1:e.includes("\u0432\u0435\u0440\u0441\u0438\u044F")||e.includes("version")?2:/\(([^)]*)\)/.test(e)?3:4}scoreSearchCandidate(s,e,o){const r=this.normalizeQuery(o),t=this.normalizeQuery(s),a=this.normalizeQuery(e),l=`${a} ${t}`;let i=0;const c=[/популярн(ое|ый)\s*вк/i,/\bvk\b/i,/tiktok/i,/instagram/i,/8d/i,/bass\s*boost/i,/nightcore/i,/slowed/i,/sped\s*up/i,/lyrics|текст/i,/radio edit/i];c.some(n=>n.test(t))&&(i-=60),[/\b(минус|instrumental|karaoke|cover|remix|mix|rington|рингтон)\b/i,/\(([^)]*(минус|remix|cover|instrumental|8d)[^)]*)\)/i].some(n=>n.test(t))&&(i-=35);const d=r.split(/\s+/).filter(Boolean).filter(n=>l.includes(n)).length,u=this.getTrackType(s);return i+=(4-u)*20,i+=d*10,t===r&&(i+=120),t.startsWith(r)&&(i+=70),t.includes(r)&&(i+=40),a.includes(r)&&(i+=25),!this.isBadVariant(t)&&!c.some(n=>n.test(t))&&!t.includes("(")&&(i+=30),t.replace(/\([^)]*\)/g,"").trim()===r&&(i+=15),t.length<40&&(i+=5),i}}
2
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/format.ts"],
4
+ "sourcesContent": ["import { Retry } from \"./retry.js\";\n\nexport class Format {\n private retry: Retry;\n\n constructor() {\n this.retry = new Retry();\n }\n\n normalizeQuery(query: string): string {\n return this.retry.normalizeText(query).toLowerCase();\n }\n\n isBadVariant(title: string): boolean {\n const t = title.toLowerCase();\n\n const badPatterns = [\n /\\b(\u043C\u0438\u043D\u0443\u0441|instrumental|karaoke|\u043A\u0430\u0440\u0430\u043E\u043A\u0435|remix|mix|bassboosted|rington|\u0440\u0438\u043D\u0433\u0442\u043E\u043D|cover|live|lyrics?|\u0442\u0435\u043A\u0441\u0442|version|ver\\.?|slowed|sped\\s*up|nightcore)\\b/i,\n /\\((?:[^)]*\\b(?:\u043C\u0438\u043D\u0443\u0441|instrumental|karaoke|remix|mix|bassboosted|rington|\u0440\u0438\u043D\u0433\u0442\u043E\u043D|cover|live|lyrics?|\u0442\u0435\u043A\u0441\u0442|version|ver\\.?|slowed|sped\\s*up|nightcore)\\b[^)]*)\\)/i,\n ];\n\n return badPatterns.some((re) => re.test(t));\n }\n\n private getTrackType(title: string): number {\n const t = this.normalizeQuery(title);\n\n if (!this.isBadVariant(t) && !t.includes(\"(\")) return 0;\n\n if (t.includes(\"2017\") || t.includes(\"album\") || t.includes(\"original\")) return 1;\n\n if (t.includes(\"\u0432\u0435\u0440\u0441\u0438\u044F\") || t.includes(\"version\")) return 2;\n\n if (/\\(([^)]*)\\)/.test(t)) return 3;\n\n return 4;\n }\n\n scoreSearchCandidate(title: string, artist: string, query: string): number {\n const q = this.normalizeQuery(query);\n const t = this.normalizeQuery(title);\n const a = this.normalizeQuery(artist);\n const haystack = `${a} ${t}`;\n\n let score = 0;\n\n const hardNoise = [\n /\u043F\u043E\u043F\u0443\u043B\u044F\u0440\u043D(\u043E\u0435|\u044B\u0439)\\s*\u0432\u043A/i,\n /\\bvk\\b/i,\n /tiktok/i,\n /instagram/i,\n /8d/i,\n /bass\\s*boost/i,\n /nightcore/i,\n /slowed/i,\n /sped\\s*up/i,\n /lyrics|\u0442\u0435\u043A\u0441\u0442/i,\n /radio edit/i,\n ];\n\n if (hardNoise.some((r) => r.test(t))) {\n score -= 60;\n }\n\n const badVariants = [\n /\\b(\u043C\u0438\u043D\u0443\u0441|instrumental|karaoke|cover|remix|mix|rington|\u0440\u0438\u043D\u0433\u0442\u043E\u043D)\\b/i,\n /\\(([^)]*(\u043C\u0438\u043D\u0443\u0441|remix|cover|instrumental|8d)[^)]*)\\)/i,\n ];\n\n if (badVariants.some((r) => r.test(t))) {\n score -= 35;\n }\n\n const qTokens = q.split(/\\s+/).filter(Boolean);\n const hits = qTokens.filter((x) => haystack.includes(x)).length;\n\n const type = this.getTrackType(title);\n\n score += (4 - type) * 20;\n\n score += hits * 10;\n\n if (t === q) score += 120;\n if (t.startsWith(q)) score += 70;\n if (t.includes(q)) score += 40;\n\n if (a.includes(q)) score += 25;\n\n const isCanonical =\n !this.isBadVariant(t) && !hardNoise.some((r) => r.test(t)) && !t.includes(\"(\");\n\n if (isCanonical) {\n score += 30;\n }\n\n const baseTitle = t.replace(/\\([^)]*\\)/g, \"\").trim();\n if (baseTitle === q) score += 15;\n\n if (t.length < 40) score += 5;\n\n return score;\n }\n}\n"],
5
+ "mappings": "aAAA,OAAS,SAAAA,MAAa,aAEhB,aAAO,MAAM,CACT,MAER,aAAA,CACE,KAAK,MAAQ,IAAIA,CACnB,CAEA,eAAeC,EAAa,CAC1B,OAAO,KAAK,MAAM,cAAcA,CAAK,EAAE,YAAW,CACpD,CAEA,aAAaC,EAAa,CACxB,MAAMC,EAAID,EAAM,YAAW,EAO3B,MALoB,CAClB,qJACA,kKAGiB,KAAME,GAAOA,EAAG,KAAKD,CAAC,CAAC,CAC5C,CAEQ,aAAaD,EAAa,CAChC,MAAMC,EAAI,KAAK,eAAeD,CAAK,EAEnC,MAAI,CAAC,KAAK,aAAaC,CAAC,GAAK,CAACA,EAAE,SAAS,GAAG,EAAU,EAElDA,EAAE,SAAS,MAAM,GAAKA,EAAE,SAAS,OAAO,GAAKA,EAAE,SAAS,UAAU,EAAU,EAE5EA,EAAE,SAAS,sCAAQ,GAAKA,EAAE,SAAS,SAAS,EAAU,EAEtD,cAAc,KAAKA,CAAC,EAAU,EAE3B,CACT,CAEA,qBAAqBD,EAAeG,EAAgBJ,EAAa,CAC/D,MAAMK,EAAI,KAAK,eAAeL,CAAK,EAC7B,EAAI,KAAK,eAAeC,CAAK,EAC7B,EAAI,KAAK,eAAeG,CAAM,EAC9BE,EAAW,GAAG,CAAC,IAAI,CAAC,GAE1B,IAAIC,EAAQ,EAEZ,MAAMC,EAAY,CAChB,wBACA,UACA,UACA,aACA,MACA,gBACA,aACA,UACA,aACA,gBACA,eAGEA,EAAU,KAAMC,GAAMA,EAAE,KAAK,CAAC,CAAC,IACjCF,GAAS,IAGS,CAClB,oEACA,wDAGc,KAAME,GAAMA,EAAE,KAAK,CAAC,CAAC,IACnCF,GAAS,IAIX,MAAMG,EADUL,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO,EACxB,OAAQM,GAAML,EAAS,SAASK,CAAC,CAAC,EAAE,OAEnDC,EAAO,KAAK,aAAaX,CAAK,EAEpC,OAAAM,IAAU,EAAIK,GAAQ,GAEtBL,GAASG,EAAO,GAEZ,IAAML,IAAGE,GAAS,KAClB,EAAE,WAAWF,CAAC,IAAGE,GAAS,IAC1B,EAAE,SAASF,CAAC,IAAGE,GAAS,IAExB,EAAE,SAASF,CAAC,IAAGE,GAAS,IAG1B,CAAC,KAAK,aAAa,CAAC,GAAK,CAACC,EAAU,KAAMC,GAAMA,EAAE,KAAK,CAAC,CAAC,GAAK,CAAC,EAAE,SAAS,GAAG,IAG7EF,GAAS,IAGO,EAAE,QAAQ,aAAc,EAAE,EAAE,KAAI,IAChCF,IAAGE,GAAS,IAE1B,EAAE,OAAS,KAAIA,GAAS,GAErBA,CACT",
6
+ "names": ["Retry", "query", "title", "t", "re", "artist", "q", "haystack", "score", "hardNoise", "r", "hits", "x", "type"]
7
+ }
package/core/http.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { HyperClient, Request } from "hyperttp";
2
+ export declare class HttpClient {
3
+ private readonly client;
4
+ constructor(client?: HyperClient);
5
+ getText(req: Request): Promise<string>;
6
+ getJson<T>(req: Request): Promise<T>;
7
+ }
8
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAGhD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,MAAM,CAAC,EAAE,WAAW;IAI1B,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAItC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;CAG3C"}
package/core/http.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";import{HyperClient as e}from"hyperttp";import{DEFAULT_HTTP_CONFIG as n}from"./config.js";export class HttpClient{client;constructor(t){this.client=t??new e(n)}async getText(t){return this.client.get(t,"text")}async getJson(t){return this.client.get(t,"json")}}
2
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/http.ts"],
4
+ "sourcesContent": ["import { HyperClient, Request } from \"hyperttp\";\nimport { DEFAULT_HTTP_CONFIG } from \"./config.js\";\n\nexport class HttpClient {\n private readonly client: HyperClient;\n\n constructor(client?: HyperClient) {\n this.client = client ?? new HyperClient(DEFAULT_HTTP_CONFIG);\n }\n\n async getText(req: Request): Promise<string> {\n return this.client.get<string>(req, \"text\");\n }\n\n async getJson<T>(req: Request): Promise<T> {\n return this.client.get<T>(req, \"json\");\n }\n}\n"],
5
+ "mappings": "aAAA,OAAS,eAAAA,MAA4B,WACrC,OAAS,uBAAAC,MAA2B,cAE9B,aAAO,UAAU,CACJ,OAEjB,YAAYC,EAAoB,CAC9B,KAAK,OAASA,GAAU,IAAIF,EAAYC,CAAmB,CAC7D,CAEA,MAAM,QAAQE,EAAY,CACxB,OAAO,KAAK,OAAO,IAAYA,EAAK,MAAM,CAC5C,CAEA,MAAM,QAAWA,EAAY,CAC3B,OAAO,KAAK,OAAO,IAAOA,EAAK,MAAM,CACvC",
6
+ "names": ["HyperClient", "DEFAULT_HTTP_CONFIG", "client", "req"]
7
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export { Format } from "./format.js";
3
+ export { Retry } from "./retry.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAE3B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
package/core/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";export*from"./types.js";export{Format}from"./format.js";export{Retry}from"./retry.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/index.ts"],
4
+ "sourcesContent": ["export * from \"./types.js\";\n\nexport { Format } from \"./format.js\";\nexport { Retry } from \"./retry.js\";\n"],
5
+ "mappings": "aAAA,WAAc,aAEd,OAAS,WAAc,cACvB,OAAS,UAAa",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ export declare function normalizeText(value?: string | null): string;
2
+ export declare function normalizeQuery(value: string): string;
3
+ export declare function normalizeTrackId(input: string): string;
4
+ export declare function normalizeTitle(input: string): string;
5
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/core/normalize.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE3D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD"}
@@ -0,0 +1,2 @@
1
+ "use strict";export function normalizeText(e){return(e??"").replace(/\s+/g," ").trim()}export function normalizeQuery(e){return normalizeText(e).toLowerCase()}export function normalizeTrackId(e){return e.replace("hitmos:track:","").replace("hitmoz:track:","").trim()}export function normalizeTitle(e){return e.toLowerCase().replace(/[(),.-]/g," ").replace(/\s+/g," ").replace(/\b(remix|edit|version|bassboosted|8d|slowed|speed up)\b/g,"").trim()}
2
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/normalize.ts"],
4
+ "sourcesContent": ["export function normalizeText(value?: string | null): string {\n return (value ?? \"\").replace(/\\s+/g, \" \").trim();\n}\n\nexport function normalizeQuery(value: string): string {\n return normalizeText(value).toLowerCase();\n}\n\nexport function normalizeTrackId(input: string): string {\n return input.replace(\"hitmos:track:\", \"\").replace(\"hitmoz:track:\", \"\").trim();\n}\n\nexport function normalizeTitle(input: string): string {\n return input\n .toLowerCase()\n .replace(/[(),.-]/g, \" \")\n .replace(/\\s+/g, \" \")\n .replace(/\\b(remix|edit|version|bassboosted|8d|slowed|speed up)\\b/g, \"\")\n .trim();\n}\n"],
5
+ "mappings": "aAAM,gBAAU,cAAcA,EAAqB,CACjD,OAAQA,GAAS,IAAI,QAAQ,OAAQ,GAAG,EAAE,KAAI,CAChD,CAEM,gBAAU,eAAeA,EAAa,CAC1C,OAAO,cAAcA,CAAK,EAAE,YAAW,CACzC,CAEM,gBAAU,iBAAiBC,EAAa,CAC5C,OAAOA,EAAM,QAAQ,gBAAiB,EAAE,EAAE,QAAQ,gBAAiB,EAAE,EAAE,KAAI,CAC7E,CAEM,gBAAU,eAAeA,EAAa,CAC1C,OAAOA,EACJ,YAAW,EACX,QAAQ,WAAY,GAAG,EACvB,QAAQ,OAAQ,GAAG,EACnB,QAAQ,2DAA4D,EAAE,EACtE,KAAI,CACT",
6
+ "names": ["value", "input"]
7
+ }
@@ -0,0 +1,5 @@
1
+ export interface ResolvedHost {
2
+ host: string;
3
+ provider: string;
4
+ }
5
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/core/resolver.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1,2 @@
1
+ "use strict";export{};
2
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["resolver.js"],
4
+ "sourcesContent": ["export {};\n//# sourceMappingURL=resolver.js.map"],
5
+ "mappings": "aAAA",
6
+ "names": []
7
+ }
@@ -0,0 +1,46 @@
1
+ import { HyperClient } from "hyperttp";
2
+ import { type HTMLElement } from "node-html-parser";
3
+ export interface HostCacheState {
4
+ value: string | null;
5
+ expiresAt: number;
6
+ }
7
+ export declare class Retry {
8
+ private readonly inflight;
9
+ private readonly pageCache;
10
+ private readonly http;
11
+ private sessionCookie?;
12
+ private hostPromise;
13
+ private hostCache;
14
+ constructor(options?: {
15
+ httpClient?: HyperClient;
16
+ sessionCookie?: string;
17
+ });
18
+ setSessionCookie(cookie: string): void;
19
+ private sleep;
20
+ withRetry<T>(fn: () => Promise<T>, options?: {
21
+ attempts?: number;
22
+ baseDelayMs?: number;
23
+ maxDelayMs?: number;
24
+ retryOn?: (error: unknown) => boolean;
25
+ }): Promise<T>;
26
+ asText(value: unknown): string;
27
+ normalizeText(value?: string | null): string;
28
+ parseDuration(durationStr: string): number;
29
+ parseOgTitle(ogTitle: string): {
30
+ title: string;
31
+ artist: string;
32
+ };
33
+ extractDuration(root: HTMLElement): number;
34
+ extractImage(host: string, root: HTMLElement): string;
35
+ private extractRscImage;
36
+ dedupe<T>(key: string, fn: () => Promise<T>): Promise<T>;
37
+ invalidateHost(): void;
38
+ probeHost(candidateHost: string): Promise<string | null>;
39
+ resolveHost(forceRefresh?: boolean): Promise<string>;
40
+ fetchTrackPage(trackId: string): Promise<{
41
+ html: string;
42
+ root: HTMLElement;
43
+ host: string;
44
+ }>;
45
+ }
46
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/core/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAW,MAAM,UAAU,CAAC;AAChD,OAAO,EAAS,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI3D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,aAAa,CAAC,CAAS;IAE/B,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,SAAS,CAAiD;gBAEtD,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,WAAW,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;IAK1E,gBAAgB,CAAC,MAAM,EAAE,MAAM;IAI/B,OAAO,CAAC,KAAK;IAIP,SAAS,CAAC,CAAC,EACf,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;KAClC,GACL,OAAO,CAAC,CAAC,CAAC;IA2Bb,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAI9B,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM;IAI5C,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAW1C,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IA+BhE,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAkB1C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM;IA0BrD,OAAO,CAAC,eAAe;IAkCjB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAgB9D,cAAc,IAAI,IAAI;IAMhB,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA4BxD,WAAW,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAyClD,cAAc,CAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CA2C9D"}
package/core/retry.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";import{HyperClient as f,Request as g}from"hyperttp";import{parse as u}from"node-html-parser";import{DEFAULT_HTTP_CONFIG as m}from"./config.js";import{normalizeText as h,normalizeTrackId as d}from"./normalize.js";export class Retry{inflight=new Map;pageCache=new Map;http;sessionCookie;hostPromise=null;hostCache={value:null,expiresAt:0};constructor(t){this.http=t?.httpClient??new f(m),this.sessionCookie=t?.sessionCookie}setSessionCookie(t){this.sessionCookie=t}sleep(t){return new Promise(e=>setTimeout(e,t))}async withRetry(t,e={}){const s=e.attempts??3,r=e.baseDelayMs??300,n=e.maxDelayMs??3e3,i=e.retryOn??(()=>!0);let o;for(let a=1;a<=s;a++)try{return await t()}catch(c){if(o=c,a>=s||!i(c))throw c;const l=Math.floor(Math.random()*150),p=Math.min(n,r*2**(a-1)+l);await this.sleep(p)}throw o}asText(t){return typeof t=="string"?t:String(t??"")}normalizeText(t){return h(t)}parseDuration(t){if(!t)return 0;const e=t.match(/(\d+):(\d+)/);return e?parseInt(e[1],10)*60+parseInt(e[2],10):parseInt(t,10)||0}parseOgTitle(t){const e=h(t).replace(/\s*-\s*скачать.*$/i,"").replace(/\s*-\s*download.*$/i,"").trim();if(!e)return{title:"Unknown",artist:"Unknown"};const s=e.match(/^(.+?)\s*\((.+?)\)$/);if(s)return{title:h(s[1]),artist:h(s[2])};const r=e.split(" - ").map(n=>h(n)).filter(Boolean);return r.length>=2?{artist:r[0],title:r.slice(1).join(" - ")}:{title:e,artist:"Unknown"}}extractDuration(t){const e=[t.querySelector("span.shrink-0")?.textContent,t.querySelector(".track__fulltime")?.textContent,t.querySelector("[class*=duration]")?.textContent,t.querySelector("time")?.textContent,t.querySelector(".section span.text-gray")?.textContent];for(const s of e){if(!s)continue;const r=this.parseDuration(s.trim());if(r>0)return r}return 0}extractImage(t,e){const s=e.querySelector('meta[property="og:image"]')?.getAttribute("content")?.trim()||"";if(s&&!s.includes("default")&&!s.includes("android-chrome")&&!s.endsWith(".svg"))return s.startsWith("http")?s:`https://${t}${s}`;const r=e.querySelector(".section img[src*='cover'], .section img[src*='art'], img");if(r){const i=r.getAttribute("src")||"";if(i)return i.startsWith("http")?i:`https://${t}${i}`}const n=this.extractRscImage(e.innerHTML);return n||""}extractRscImage(t){const e=[],s=/self\.__next_f\.push\(\[1,"(.*?)"\]\)/gs;let r;for(;(r=s.exec(t))!==null;){const a=[...r[1].replace(/\\u003c/g,"<").replace(/\\u003e/g,">").replace(/\\u0026/g,"&").replace(/\\(["\\/bfnrt])/g,"$1").replace(/\\"/g,'"').matchAll(/"imageUrl":"(https?:\/\/[^"]+_large\.jpg)"/g)];for(const c of a){const l=c[1];!l.includes("radio")&&!l.includes("default")&&!l.includes("android-chrome")&&e.push(l)}}if(e.length===0)return null;const n=e.find(o=>o.includes("/album/"));if(n)return n;const i=e.find(o=>o.includes("/collection/"));return i||e[0]}async dedupe(t,e){const s=this.inflight.get(t);if(s)return s;const r=(async()=>{try{return await e()}finally{this.inflight.delete(t)}})();return this.inflight.set(t,r),r}invalidateHost(){this.hostCache.value=null,this.hostCache.expiresAt=0,this.pageCache.clear()}async probeHost(t){try{const e=await fetch(`https://${t}/`,{redirect:"follow"}),s=await e.text(),r=u(s),n=r.querySelector('link[rel="canonical"]')?.getAttribute("href")||r.querySelector('meta[property="og:url"]')?.getAttribute("content")||"";if(n)try{return new URL(n).host}catch{}return new URL(e.url).host}catch{return null}}async resolveHost(t=!1){const e=Date.now();if(!t&&this.hostCache.value&&this.hostCache.expiresAt>e)return this.hostCache.value;if(this.hostPromise&&!t)return this.hostPromise;this.hostPromise=(async()=>{const s=["rus.hitmos.fm","hitmos.fm","www.hitmos.fm"];for(const n of s){const i=await this.probeHost(n);if(i)return this.hostCache={value:i,expiresAt:Date.now()+30*6e4},i}const r="rus.hitmos.fm";return this.hostCache={value:r,expiresAt:Date.now()+10*6e4},r})();try{return await this.hostPromise}finally{this.hostPromise=null}}async fetchTrackPage(t){const e=d(t),s=`page:${e}`,r=this.pageCache.get(s);if(r){const n=await this.resolveHost();return{html:r,root:u(r),host:n}}try{const n=await this.resolveHost(),i=new g({scheme:"https",host:n,port:443,path:`/song/${e}`,headers:{"User-Agent":m.userAgent,Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",Referer:`https://${n}/`,...this.sessionCookie&&{Cookie:`sid=${this.sessionCookie}`}}}),o=this.asText(await this.withRetry(()=>this.http.get(i,"text"),{attempts:3,retryOn:a=>{const c=String(a?.message??a);return/timeout|ECONNRESET|429|5\d\d|network|ENOTFOUND|ETIMEDOUT/i.test(c)}}));return this.pageCache.set(s,o),{html:o,root:u(o),host:n}}catch(n){throw this.invalidateHost(),n}}}
2
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/retry.ts"],
4
+ "sourcesContent": ["import { HyperClient, Request } from \"hyperttp\";\nimport { parse, type HTMLElement } from \"node-html-parser\";\nimport { DEFAULT_HTTP_CONFIG } from \"./config.js\";\nimport { normalizeText, normalizeTrackId } from \"./normalize.js\";\n\nexport interface HostCacheState {\n value: string | null;\n expiresAt: number;\n}\n\nexport class Retry {\n private readonly inflight = new Map<string, Promise<any>>();\n private readonly pageCache = new Map<string, string>();\n private readonly http: HyperClient;\n private sessionCookie?: string;\n\n private hostPromise: Promise<string> | null = null;\n private hostCache: HostCacheState = { value: null, expiresAt: 0 };\n\n constructor(options?: { httpClient?: HyperClient; sessionCookie?: string }) {\n this.http = options?.httpClient ?? new HyperClient(DEFAULT_HTTP_CONFIG);\n this.sessionCookie = options?.sessionCookie;\n }\n\n setSessionCookie(cookie: string) {\n this.sessionCookie = cookie;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n async withRetry<T>(\n fn: () => Promise<T>,\n options: {\n attempts?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (error: unknown) => boolean;\n } = {},\n ): Promise<T> {\n const attempts = options.attempts ?? 3;\n const baseDelayMs = options.baseDelayMs ?? 300;\n const maxDelayMs = options.maxDelayMs ?? 3000;\n const retryOn = options.retryOn ?? (() => true);\n\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= attempts || !retryOn(error)) {\n throw error;\n }\n\n const jitter = Math.floor(Math.random() * 150);\n const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1) + jitter);\n await this.sleep(delay);\n }\n }\n\n throw lastError;\n }\n\n asText(value: unknown): string {\n return typeof value === \"string\" ? value : String(value ?? \"\");\n }\n\n normalizeText(value?: string | null): string {\n return normalizeText(value);\n }\n\n parseDuration(durationStr: string): number {\n if (!durationStr) return 0;\n\n const match = durationStr.match(/(\\d+):(\\d+)/);\n if (match) {\n return parseInt(match[1]!, 10) * 60 + parseInt(match[2]!, 10);\n }\n\n return parseInt(durationStr, 10) || 0;\n }\n\n parseOgTitle(ogTitle: string): { title: string; artist: string } {\n const cleaned = normalizeText(ogTitle)\n .replace(/\\s*-\\s*\u0441\u043A\u0430\u0447\u0430\u0442\u044C.*$/i, \"\")\n .replace(/\\s*-\\s*download.*$/i, \"\")\n .trim();\n\n if (!cleaned) return { title: \"Unknown\", artist: \"Unknown\" };\n\n const parenMatch = cleaned.match(/^(.+?)\\s*\\((.+?)\\)$/);\n if (parenMatch) {\n return {\n title: normalizeText(parenMatch[1]),\n artist: normalizeText(parenMatch[2]),\n };\n }\n\n const dashParts = cleaned\n .split(\" - \")\n .map((part) => normalizeText(part))\n .filter(Boolean);\n\n if (dashParts.length >= 2) {\n return {\n artist: dashParts[0]!,\n title: dashParts.slice(1).join(\" - \"),\n };\n }\n\n return { title: cleaned, artist: \"Unknown\" };\n }\n\n extractDuration(root: HTMLElement): number {\n const candidates = [\n root.querySelector(\"span.shrink-0\")?.textContent,\n root.querySelector(\".track__fulltime\")?.textContent,\n root.querySelector(\"[class*=duration]\")?.textContent,\n root.querySelector(\"time\")?.textContent,\n root.querySelector(\".section span.text-gray\")?.textContent,\n ];\n\n for (const candidate of candidates) {\n if (!candidate) continue;\n const parsed = this.parseDuration(candidate.trim());\n if (parsed > 0) return parsed;\n }\n\n return 0;\n }\n\n extractImage(host: string, root: HTMLElement): string {\n const ogImage =\n root.querySelector('meta[property=\"og:image\"]')?.getAttribute(\"content\")?.trim() || \"\";\n\n if (\n ogImage &&\n !ogImage.includes(\"default\") &&\n !ogImage.includes(\"android-chrome\") &&\n !ogImage.endsWith(\".svg\")\n ) {\n return ogImage.startsWith(\"http\") ? ogImage : `https://${host}${ogImage}`;\n }\n\n const imgEl = root.querySelector(\".section img[src*='cover'], .section img[src*='art'], img\");\n\n if (imgEl) {\n const src = imgEl.getAttribute(\"src\") || \"\";\n if (src) return src.startsWith(\"http\") ? src : `https://${host}${src}`;\n }\n\n const rscImages = this.extractRscImage(root.innerHTML);\n if (rscImages) return rscImages;\n\n return \"\";\n }\n\n private extractRscImage(html: string): string | null {\n const imageUrls: string[] = [];\n const rscRegex = /self\\.__next_f\\.push\\(\\[1,\"(.*?)\"\\]\\)/gs;\n let match: RegExpExecArray | null;\n\n while ((match = rscRegex.exec(html)) !== null) {\n const payload = match[1]!\n .replace(/\\\\u003c/g, \"<\")\n .replace(/\\\\u003e/g, \">\")\n .replace(/\\\\u0026/g, \"&\")\n .replace(/\\\\([\"\\\\/bfnrt])/g, \"$1\")\n .replace(/\\\\\"/g, '\"');\n\n const urls = [...payload.matchAll(/\"imageUrl\":\"(https?:\\/\\/[^\"]+_large\\.jpg)\"/g)];\n\n for (const u of urls) {\n const url = u[1]!;\n if (!url.includes(\"radio\") && !url.includes(\"default\") && !url.includes(\"android-chrome\")) {\n imageUrls.push(url);\n }\n }\n }\n\n if (imageUrls.length === 0) return null;\n\n const albumArt = imageUrls.find((u) => u.includes(\"/album/\"));\n if (albumArt) return albumArt;\n\n const artistArt = imageUrls.find((u) => u.includes(\"/collection/\"));\n if (artistArt) return artistArt;\n\n return imageUrls[0]!;\n }\n\n async dedupe<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const existing = this.inflight.get(key);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n return await fn();\n } finally {\n this.inflight.delete(key);\n }\n })();\n\n this.inflight.set(key, promise);\n return promise;\n }\n\n invalidateHost(): void {\n this.hostCache.value = null;\n this.hostCache.expiresAt = 0;\n this.pageCache.clear();\n }\n\n async probeHost(candidateHost: string): Promise<string | null> {\n try {\n const res = await fetch(`https://${candidateHost}/`, {\n redirect: \"follow\",\n });\n\n const html = await res.text();\n const root = parse(html);\n\n const canonical =\n root.querySelector('link[rel=\"canonical\"]')?.getAttribute(\"href\") ||\n root.querySelector('meta[property=\"og:url\"]')?.getAttribute(\"content\") ||\n \"\";\n\n if (canonical) {\n try {\n return new URL(canonical).host;\n } catch {\n // ignore\n }\n }\n\n return new URL(res.url).host;\n } catch {\n return null;\n }\n }\n\n async resolveHost(forceRefresh = false): Promise<string> {\n const now = Date.now();\n\n if (!forceRefresh && this.hostCache.value && this.hostCache.expiresAt > now) {\n return this.hostCache.value;\n }\n\n if (this.hostPromise && !forceRefresh) {\n return this.hostPromise;\n }\n\n this.hostPromise = (async (): Promise<string> => {\n const candidates = [\"rus.hitmos.fm\", \"hitmos.fm\", \"www.hitmos.fm\"];\n\n for (const candidate of candidates) {\n const host = await this.probeHost(candidate);\n\n if (host) {\n this.hostCache = {\n value: host,\n expiresAt: Date.now() + 30 * 60_000,\n };\n return host;\n }\n }\n\n const fallbackHost = \"rus.hitmos.fm\";\n this.hostCache = {\n value: fallbackHost,\n expiresAt: Date.now() + 10 * 60_000,\n };\n return fallbackHost;\n })();\n\n try {\n return await this.hostPromise;\n } finally {\n this.hostPromise = null;\n }\n }\n\n async fetchTrackPage(\n trackId: string,\n ): Promise<{ html: string; root: HTMLElement; host: string }> {\n const id = normalizeTrackId(trackId);\n const cacheKey = `page:${id}`;\n\n const cached = this.pageCache.get(cacheKey);\n if (cached) {\n const host = await this.resolveHost();\n return { html: cached, root: parse(cached), host };\n }\n\n try {\n const host = await this.resolveHost();\n\n const req = new Request({\n scheme: \"https\",\n host,\n port: 443,\n path: `/song/${id}`,\n headers: {\n \"User-Agent\": DEFAULT_HTTP_CONFIG.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n Referer: `https://${host}/`,\n ...(this.sessionCookie && { Cookie: `sid=${this.sessionCookie}` }),\n },\n });\n\n const html = this.asText(\n await this.withRetry(() => this.http.get(req, \"text\"), {\n attempts: 3,\n retryOn: (error) => {\n const msg = String((error as any)?.message ?? error);\n return /timeout|ECONNRESET|429|5\\d\\d|network|ENOTFOUND|ETIMEDOUT/i.test(msg);\n },\n }),\n );\n\n this.pageCache.set(cacheKey, html);\n return { html, root: parse(html), host };\n } catch (error) {\n this.invalidateHost();\n throw error;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["HyperClient", "Request", "parse", "DEFAULT_HTTP_CONFIG", "normalizeText", "normalizeTrackId", "options", "cookie", "ms", "resolve", "fn", "attempts", "baseDelayMs", "maxDelayMs", "retryOn", "lastError", "attempt", "error", "jitter", "delay", "value", "durationStr", "match", "ogTitle", "cleaned", "parenMatch", "dashParts", "part", "root", "candidates", "candidate", "parsed", "host", "ogImage", "imgEl", "src", "rscImages", "html", "imageUrls", "rscRegex", "urls", "u", "url", "albumArt", "artistArt", "key", "existing", "promise", "candidateHost", "res", "canonical", "forceRefresh", "now", "fallbackHost", "trackId", "id", "cacheKey", "cached", "req", "msg"]
7
+ }
@@ -0,0 +1,5 @@
1
+ import type { TrackMeta } from "../models/track.js";
2
+ export declare function getTrackType(title: string): number;
3
+ export declare function scoreSearchCandidate(title: string, artist: string, query: string): number;
4
+ export declare function sortTracksByScore(query: string, tracks: TrackMeta[]): TrackMeta[];
5
+ //# sourceMappingURL=scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/core/scoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAIpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYlD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiDzF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CASjF"}
@@ -0,0 +1,2 @@
1
+ "use strict";import{isBadTitle as l}from"./filters.js";import{normalizeQuery as c}from"./normalize.js";export function getTrackType(n){const i=c(n);return!l(i)&&!i.includes("(")?0:i.includes("2017")||i.includes("album")||i.includes("original")?1:i.includes("version")||i.includes("\u0432\u0435\u0440\u0441\u0438\u044F")?2:/\(([^)]*)\)/.test(i)?3:4}export function scoreSearchCandidate(n,i,t){const e=c(t),r=c(n),a=c(i),u=`${a} ${r}`;let s=0;const d=[/популярн(ое|ый)\s*вк/i,/\bvk\b/i,/tiktok/i,/instagram/i,/8d/i,/bass\s*boost/i,/nightcore/i,/slowed/i,/sped\s*up/i,/lyrics|текст/i,/radio edit/i],f=[/\b(минус|instrumental|karaoke|cover|remix|mix|rington|рингтон)\b/i,/\(([^)]*(минус|remix|cover|instrumental|8d)[^)]*)\)/i];d.some(o=>o.test(r))&&(s-=60),f.some(o=>o.test(r))&&(s-=35);const m=e.split(/\s+/).filter(Boolean).filter(o=>u.includes(o)).length;s+=m*10,r===e&&(s+=120),r.startsWith(e)&&(s+=70),r.includes(e)&&(s+=40),a.includes(e)&&(s+=25);const p=getTrackType(n);return s+=(4-p)*20,r.replace(/\([^)]*\)/g,"").trim()===e&&(s+=15),r.length<40&&(s+=5),s}export function sortTracksByScore(n,i){return i.map(t=>({track:t,score:scoreSearchCandidate(t.title,t.artist,n)})).filter(({score:t,track:e})=>t>=8&&!l(e.title)).sort((t,e)=>e.score-t.score||e.track.duration-t.track.duration).map(({track:t})=>t)}
2
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/scoring.ts"],
4
+ "sourcesContent": ["import type { TrackMeta } from \"../models/track.js\";\nimport { isBadTitle } from \"./filters.js\";\nimport { normalizeQuery } from \"./normalize.js\";\n\nexport function getTrackType(title: string): number {\n const t = normalizeQuery(title);\n\n if (!isBadTitle(t) && !t.includes(\"(\")) return 0;\n\n if (t.includes(\"2017\") || t.includes(\"album\") || t.includes(\"original\")) return 1;\n\n if (t.includes(\"version\") || t.includes(\"\u0432\u0435\u0440\u0441\u0438\u044F\")) return 2;\n\n if (/\\(([^)]*)\\)/.test(t)) return 3;\n\n return 4;\n}\n\nexport function scoreSearchCandidate(title: string, artist: string, query: string): number {\n const q = normalizeQuery(query);\n const t = normalizeQuery(title);\n const a = normalizeQuery(artist);\n const haystack = `${a} ${t}`;\n\n let score = 0;\n\n const hardNoise = [\n /\u043F\u043E\u043F\u0443\u043B\u044F\u0440\u043D(\u043E\u0435|\u044B\u0439)\\s*\u0432\u043A/i,\n /\\bvk\\b/i,\n /tiktok/i,\n /instagram/i,\n /8d/i,\n /bass\\s*boost/i,\n /nightcore/i,\n /slowed/i,\n /sped\\s*up/i,\n /lyrics|\u0442\u0435\u043A\u0441\u0442/i,\n /radio edit/i,\n ];\n\n const badVariants = [\n /\\b(\u043C\u0438\u043D\u0443\u0441|instrumental|karaoke|cover|remix|mix|rington|\u0440\u0438\u043D\u0433\u0442\u043E\u043D)\\b/i,\n /\\(([^)]*(\u043C\u0438\u043D\u0443\u0441|remix|cover|instrumental|8d)[^)]*)\\)/i,\n ];\n\n if (hardNoise.some((r) => r.test(t))) score -= 60;\n if (badVariants.some((r) => r.test(t))) score -= 35;\n\n const qTokens = q.split(/\\s+/).filter(Boolean);\n const hits = qTokens.filter((x) => haystack.includes(x)).length;\n\n score += hits * 10;\n\n if (t === q) score += 120;\n if (t.startsWith(q)) score += 70;\n if (t.includes(q)) score += 40;\n if (a.includes(q)) score += 25;\n\n const type = getTrackType(title);\n score += (4 - type) * 20;\n\n const baseTitle = t.replace(/\\([^)]*\\)/g, \"\").trim();\n if (baseTitle === q) score += 15;\n\n if (t.length < 40) score += 5;\n\n return score;\n}\n\nexport function sortTracksByScore(query: string, tracks: TrackMeta[]): TrackMeta[] {\n return tracks\n .map((track) => ({\n track,\n score: scoreSearchCandidate(track.title, track.artist, query),\n }))\n .filter(({ score, track }) => score >= 8 && !isBadTitle(track.title))\n .sort((a, b) => b.score - a.score || b.track.duration - a.track.duration)\n .map(({ track }) => track);\n}\n"],
5
+ "mappings": "aACA,OAAS,cAAAA,MAAkB,eAC3B,OAAS,kBAAAC,MAAsB,iBAEzB,gBAAU,aAAaC,EAAa,CACxC,MAAMC,EAAIF,EAAeC,CAAK,EAE9B,MAAI,CAACF,EAAWG,CAAC,GAAK,CAACA,EAAE,SAAS,GAAG,EAAU,EAE3CA,EAAE,SAAS,MAAM,GAAKA,EAAE,SAAS,OAAO,GAAKA,EAAE,SAAS,UAAU,EAAU,EAE5EA,EAAE,SAAS,SAAS,GAAKA,EAAE,SAAS,sCAAQ,EAAU,EAEtD,cAAc,KAAKA,CAAC,EAAU,EAE3B,CACT,CAEM,gBAAU,qBAAqBD,EAAeE,EAAgBC,EAAa,CAC/E,MAAMC,EAAIL,EAAeI,CAAK,EACxBF,EAAIF,EAAeC,CAAK,EACxB,EAAID,EAAeG,CAAM,EACzBG,EAAW,GAAG,CAAC,IAAIJ,CAAC,GAE1B,IAAIK,EAAQ,EAEZ,MAAMC,EAAY,CAChB,wBACA,UACA,UACA,aACA,MACA,gBACA,aACA,UACA,aACA,gBACA,eAGIC,EAAc,CAClB,oEACA,wDAGED,EAAU,KAAME,GAAMA,EAAE,KAAKR,CAAC,CAAC,IAAGK,GAAS,IAC3CE,EAAY,KAAMC,GAAMA,EAAE,KAAKR,CAAC,CAAC,IAAGK,GAAS,IAGjD,MAAMI,EADUN,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO,EACxB,OAAQO,GAAMN,EAAS,SAASM,CAAC,CAAC,EAAE,OAEzDL,GAASI,EAAO,GAEZT,IAAMG,IAAGE,GAAS,KAClBL,EAAE,WAAWG,CAAC,IAAGE,GAAS,IAC1BL,EAAE,SAASG,CAAC,IAAGE,GAAS,IACxB,EAAE,SAASF,CAAC,IAAGE,GAAS,IAE5B,MAAMM,EAAO,aAAaZ,CAAK,EAC/B,OAAAM,IAAU,EAAIM,GAAQ,GAEJX,EAAE,QAAQ,aAAc,EAAE,EAAE,KAAI,IAChCG,IAAGE,GAAS,IAE1BL,EAAE,OAAS,KAAIK,GAAS,GAErBA,CACT,CAEM,gBAAU,kBAAkBH,EAAeU,EAAmB,CAClE,OAAOA,EACJ,IAAKC,IAAW,CACf,MAAAA,EACA,MAAO,qBAAqBA,EAAM,MAAOA,EAAM,OAAQX,CAAK,GAC5D,EACD,OAAO,CAAC,CAAE,MAAAG,EAAO,MAAAQ,CAAK,IAAOR,GAAS,GAAK,CAACR,EAAWgB,EAAM,KAAK,CAAC,EACnE,KAAK,CAACC,EAAGC,IAAMA,EAAE,MAAQD,EAAE,OAASC,EAAE,MAAM,SAAWD,EAAE,MAAM,QAAQ,EACvE,IAAI,CAAC,CAAE,MAAAD,CAAK,IAAOA,CAAK,CAC7B",
6
+ "names": ["isBadTitle", "normalizeQuery", "title", "t", "artist", "query", "q", "haystack", "score", "hardNoise", "badVariants", "r", "hits", "x", "type", "tracks", "track", "a", "b"]
7
+ }
@@ -0,0 +1,5 @@
1
+ export type HostCacheState = {
2
+ value: string | null;
3
+ expiresAt: number;
4
+ };
5
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
package/core/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";export{};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["types.js"],
4
+ "sourcesContent": ["export {};\n//# sourceMappingURL=types.js.map"],
5
+ "mappings": "aAAA",
6
+ "names": []
7
+ }
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { HITApi } from "./api/HITApi.js";
2
+ export type { TrackMeta } from "./models/track.js";
3
+ export type { TrackAudio } from "./models/audio.js";
4
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";export{HITApi}from"./api/HITApi.js";
2
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export { HITApi } from \"./api/HITApi.js\";\nexport type { TrackMeta } from \"./models/track.js\";\nexport type { TrackAudio } from \"./models/audio.js\";\n"],
5
+ "mappings": "aAAA,OAAS,WAAc",
6
+ "names": []
7
+ }
@@ -0,0 +1,14 @@
1
+ export interface TrackAudioFile {
2
+ url: string;
3
+ format: string;
4
+ bitrate?: number;
5
+ }
6
+ export interface TrackAudio {
7
+ id: string;
8
+ url: string;
9
+ format: string;
10
+ files: TrackAudioFile[];
11
+ urls: string[];
12
+ expiresAt: number;
13
+ }
14
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../../src/models/audio.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,2 @@
1
+ "use strict";export{};
2
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["audio.js"],
4
+ "sourcesContent": ["export {};\n//# sourceMappingURL=audio.js.map"],
5
+ "mappings": "aAAA",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ export interface TrackMeta {
2
+ id: string;
3
+ title: string;
4
+ artist: string;
5
+ duration: number;
6
+ uri: string;
7
+ name: string;
8
+ duration_ms: number;
9
+ explicit: boolean;
10
+ image?: string;
11
+ }
12
+ //# sourceMappingURL=track.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/models/track.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,2 @@
1
+ "use strict";export{};
2
+ //# sourceMappingURL=track.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["track.js"],
4
+ "sourcesContent": ["export {};\n//# sourceMappingURL=track.js.map"],
5
+ "mappings": "aAAA",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "hitd2",
3
+ "version": "1.2.0",
4
+ "description": "A Node.js wrapper for the Hitmo API (Unofficial) http://hitmo.me",
5
+ "keywords": [
6
+ "api",
7
+ "hitmos",
8
+ "hitmos.me",
9
+ "music",
10
+ "wrapper"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Dirold2",
14
+ "contributors": [
15
+ "Dirold2 (https://github.com/dirold2)"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/dirold2/spd2.git"
20
+ },
21
+ "type": "module",
22
+ "main": "./index.js",
23
+ "types": "./index.d.ts",
24
+ "dependencies": {
25
+ "hcacher": "^0.1.0",
26
+ "hyperttp": "^0.4.11",
27
+ "node-html-parser": "^8.0.4"
28
+ },
29
+ "private": false
30
+ }
@@ -0,0 +1,11 @@
1
+ import type { HTMLElement } from "node-html-parser";
2
+ import type { TrackMeta } from "../models/track.js";
3
+ export interface SiteProvider {
4
+ readonly name: string;
5
+ readonly hosts: string[];
6
+ matches(host: string, html?: string): boolean;
7
+ parseSearch(root: HTMLElement): TrackMeta[];
8
+ parseTrackPage(root: HTMLElement, host: string, trackId: string): Partial<TrackMeta>;
9
+ extractAudioUrl(root: HTMLElement, html: string, host: string, trackId: string): string | null;
10
+ }
11
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/providers/base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAEzB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAE9C,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,CAAC;IAE5C,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAErF,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAChG"}
@@ -0,0 +1,2 @@
1
+ "use strict";export{};
2
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["base.js"],
4
+ "sourcesContent": ["export {};\n//# sourceMappingURL=base.js.map"],
5
+ "mappings": "aAAA",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { HTMLElement } from "node-html-parser";
2
+ import type { SiteProvider } from "./base.js";
3
+ import type { TrackMeta } from "../models/track.js";
4
+ export declare class HitmosProvider implements SiteProvider {
5
+ readonly name = "hitmos";
6
+ readonly hosts: string[];
7
+ matches(host: string, html?: string): boolean;
8
+ parseSearch(root: HTMLElement): TrackMeta[];
9
+ private parseSearchLegacy;
10
+ parseTrackPage(root: HTMLElement, _host: string, _trackId: string): Partial<TrackMeta>;
11
+ extractAudioUrl(_root: HTMLElement, html: string, _host: string, trackId: string): string | null;
12
+ }
13
+ //# sourceMappingURL=hitmos.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hitmos.provider.d.ts","sourceRoot":"","sources":["../../src/providers/hitmos.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAmDpD,qBAAa,cAAe,YAAW,YAAY;IACjD,QAAQ,CAAC,IAAI,YAAY;IAEzB,QAAQ,CAAC,KAAK,WAAmD;IAEjE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAQ7C,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE;IAmB3C,OAAO,CAAC,iBAAiB;IAqCzB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAOtF,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAmBjG"}
@@ -0,0 +1,2 @@
1
+ "use strict";import{normalizeText as o}from"../core/normalize.js";function d(l){const r=[],t=/<script[^>]*>self\.__next_f\.push\(\[1,"(.*?)"\]\)/gs;let e;for(;(e=t.exec(l))!==null;)r.push(e[1].replace(/\\u003c/g,"<").replace(/\\u003e/g,">").replace(/\\u0026/g,"&").replace(/\\(["\\/bfnrt])/g,"$1").replace(/\\"/g,'"'));return r}function f(l){const r=d(l);for(const t of r){const e=t.match(/"list":(\[.*?\]),\s*"footer"/);if(e)try{return JSON.parse(e[1])}catch{continue}}return null}export class HitmosProvider{name="hitmos";hosts=["hitmos.fm","rus.hitmos.fm","www.hitmos.fm"];matches(r,t){return r.includes("hitmos")||t?.includes("li.relative")===!0||t?.includes("__next_f.push")===!0}parseSearch(r){const t=f(r.innerHTML);return t&&t.length>0?t.map(e=>({id:String(e.id),title:o(e.title),artist:o(e.artist),duration:e.duration,uri:`hitmos:track:${e.id}`,name:o(e.title),duration_ms:e.duration*1e3,explicit:!1})):this.parseSearchLegacy(r)}parseSearchLegacy(r){const t=[],e=r.querySelectorAll("li.relative");for(const n of e){const s=n.querySelector('a[href^="/song/"]'),i=n.querySelector('a[href^="/artist/"]');if(!s||!i)continue;const a=(s.getAttribute("href")||"").match(/\/song\/([a-zA-Z0-9_-]+)/)?.[1];if(!a)continue;const c=o(s.textContent),u=o(i.textContent),h=(n.querySelector("span.shrink-0")?.textContent?.trim()||"").split(":"),m=h.length===2?parseInt(h[0],10)*60+parseInt(h[1],10):0;t.push({id:a,title:c,artist:u,duration:m,uri:`hitmos:track:${a}`,name:c,duration_ms:m*1e3,explicit:!1})}return t}parseTrackPage(r,t,e){const n=r.querySelector('meta[property="og:title"]')?.getAttribute("content")?.trim()||"";return n?{title:n}:{}}extractAudioUrl(r,t,e,n){const s=f(t);if(s&&s.length>0){const c=s.find(u=>String(u.id)===n&&u.play);if(c?.play)return c.play;if(s[0]?.play)return s[0].play}const i=t.match(/(https?:\/\/[^\s"'<>]+\.mp3[^\s"'<>]*)/);if(i?.[1])return i[1].replace(/\\/g,"").trim();const p=t.match(/"url"\s*:\s*"([^"]+)"/);if(p?.[1])return p[1].replace(/\\/g,"").trim();const a=t.match(/["'](\/L[a-zA-Z0-9_=]+\.mp3)["']/);return a?`https://pl1.hitmos.fm${a[1]}`.replace(/\\/g,"").trim():null}}
2
+ //# sourceMappingURL=hitmos.provider.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/providers/hitmos.provider.ts"],
4
+ "sourcesContent": ["import type { HTMLElement } from \"node-html-parser\";\nimport type { SiteProvider } from \"./base.js\";\nimport type { TrackMeta } from \"../models/track.js\";\nimport { normalizeText } from \"../core/normalize.js\";\n\ninterface RscTrack {\n id: number;\n artist: string;\n title: string;\n version?: string;\n duration: number;\n bitrate?: number;\n play?: string;\n download?: string;\n}\n\nfunction extractRscPayload(html: string): string[] {\n const payloads: string[] = [];\n const scriptRegex = /<script[^>]*>self\\.__next_f\\.push\\(\\[1,\"(.*?)\"\\]\\)/gs;\n let match: RegExpExecArray | null;\n\n while ((match = scriptRegex.exec(html)) !== null) {\n payloads.push(\n match[1]!\n .replace(/\\\\u003c/g, \"<\")\n .replace(/\\\\u003e/g, \">\")\n .replace(/\\\\u0026/g, \"&\")\n .replace(/\\\\([\"\\\\/bfnrt])/g, \"$1\")\n .replace(/\\\\\"/g, '\"'),\n );\n }\n\n return payloads;\n}\n\nfunction parseRscTrackList(html: string): RscTrack[] | null {\n const payloads = extractRscPayload(html);\n\n for (const payload of payloads) {\n const listMatch = payload.match(/\"list\":(\\[.*?\\]),\\s*\"footer\"/);\n if (listMatch) {\n try {\n const tracks: RscTrack[] = JSON.parse(listMatch[1]!);\n return tracks;\n } catch {\n continue;\n }\n }\n }\n\n return null;\n}\n\nexport class HitmosProvider implements SiteProvider {\n readonly name = \"hitmos\";\n\n readonly hosts = [\"hitmos.fm\", \"rus.hitmos.fm\", \"www.hitmos.fm\"];\n\n matches(host: string, html?: string): boolean {\n return (\n host.includes(\"hitmos\") ||\n html?.includes(\"li.relative\") === true ||\n html?.includes(\"__next_f.push\") === true\n );\n }\n\n parseSearch(root: HTMLElement): TrackMeta[] {\n const rscTracks = parseRscTrackList(root.innerHTML);\n\n if (rscTracks && rscTracks.length > 0) {\n return rscTracks.map((t) => ({\n id: String(t.id),\n title: normalizeText(t.title),\n artist: normalizeText(t.artist),\n duration: t.duration,\n uri: `hitmos:track:${t.id}`,\n name: normalizeText(t.title),\n duration_ms: t.duration * 1000,\n explicit: false,\n }));\n }\n\n return this.parseSearchLegacy(root);\n }\n\n private parseSearchLegacy(root: HTMLElement): TrackMeta[] {\n const results: TrackMeta[] = [];\n const items = root.querySelectorAll(\"li.relative\");\n\n for (const item of items) {\n const songLink = item.querySelector('a[href^=\"/song/\"]');\n const artistLink = item.querySelector('a[href^=\"/artist/\"]');\n\n if (!songLink || !artistLink) continue;\n\n const href = songLink.getAttribute(\"href\") || \"\";\n const id = href.match(/\\/song\\/([a-zA-Z0-9_-]+)/)?.[1];\n if (!id) continue;\n\n const title = normalizeText(songLink.textContent);\n const artist = normalizeText(artistLink.textContent);\n\n const durationText = item.querySelector(\"span.shrink-0\")?.textContent?.trim() || \"\";\n const parts = durationText.split(\":\");\n const duration =\n parts.length === 2 ? parseInt(parts[0]!, 10) * 60 + parseInt(parts[1]!, 10) : 0;\n\n results.push({\n id,\n title,\n artist,\n duration,\n uri: `hitmos:track:${id}`,\n name: title,\n duration_ms: duration * 1000,\n explicit: false,\n });\n }\n\n return results;\n }\n\n parseTrackPage(root: HTMLElement, _host: string, _trackId: string): Partial<TrackMeta> {\n const ogTitle =\n root.querySelector('meta[property=\"og:title\"]')?.getAttribute(\"content\")?.trim() || \"\";\n const parsed = ogTitle ? { title: ogTitle } : {};\n return parsed;\n }\n\n extractAudioUrl(_root: HTMLElement, html: string, _host: string, trackId: string): string | null {\n const rscTracks = parseRscTrackList(html);\n if (rscTracks && rscTracks.length > 0) {\n const match = rscTracks.find((t) => String(t.id) === trackId && t.play);\n if (match?.play) return match.play;\n if (rscTracks[0]?.play) return rscTracks[0].play;\n }\n\n const directMatch = html.match(/(https?:\\/\\/[^\\s\"'<>]+\\.mp3[^\\s\"'<>]*)/);\n if (directMatch?.[1]) return directMatch[1].replace(/\\\\/g, \"\").trim();\n\n const jsonMatch = html.match(/\"url\"\\s*:\\s*\"([^\"]+)\"/);\n if (jsonMatch?.[1]) return jsonMatch[1].replace(/\\\\/g, \"\").trim();\n\n const base64Match = html.match(/[\"'](\\/L[a-zA-Z0-9_=]+\\.mp3)[\"']/);\n if (base64Match) return `https://pl1.hitmos.fm${base64Match[1]}`.replace(/\\\\/g, \"\").trim();\n\n return null;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["normalizeText", "extractRscPayload", "html", "payloads", "scriptRegex", "match", "parseRscTrackList", "payload", "listMatch", "host", "root", "rscTracks", "t", "results", "items", "item", "songLink", "artistLink", "id", "title", "artist", "parts", "duration", "_host", "_trackId", "ogTitle", "_root", "trackId", "directMatch", "jsonMatch", "base64Match"]
7
+ }
@@ -0,0 +1,12 @@
1
+ import type { HTMLElement } from "node-html-parser";
2
+ import type { SiteProvider } from "./base.js";
3
+ import type { TrackMeta } from "../models/track.js";
4
+ export declare class HitmozProvider implements SiteProvider {
5
+ readonly name = "hitmoz";
6
+ readonly hosts: string[];
7
+ matches(host: string, html?: string): boolean;
8
+ parseSearch(root: HTMLElement): TrackMeta[];
9
+ parseTrackPage(root: HTMLElement, _host: string, _trackId: string): Partial<TrackMeta>;
10
+ extractAudioUrl(_root: HTMLElement, html: string, _host: string, _trackId: string): string | null;
11
+ }
12
+ //# sourceMappingURL=hitmoz.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hitmoz.provider.d.ts","sourceRoot":"","sources":["../../src/providers/hitmoz.provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,qBAAa,cAAe,YAAW,YAAY;IACjD,QAAQ,CAAC,IAAI,YAAY;IAEzB,QAAQ,CAAC,KAAK,WAAsD;IAEpE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAI7C,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE;IAsD3C,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAWtF,eAAe,CACb,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,IAAI;CAYjB"}
@@ -0,0 +1,2 @@
1
+ "use strict";import{normalizeText as i}from"../core/normalize.js";export class HitmozProvider{name="hitmoz";hosts=["rus.hitmoz.org","hitmoz.org","www.hitmoz.org"];matches(o,e){return o.includes("hitmoz")||e?.includes("tracks__item")===!0}parseSearch(o){const e=[],a=o.querySelectorAll("li.tracks__item, .tracks__item, li.relative");for(const t of a){const r=t.querySelector('a[href^="/song/"]')||t.querySelector(".track__title a")||t.querySelector(".track__title");if(!r)continue;const n=(r.getAttribute?.("href")||"").match(/\/song\/([a-zA-Z0-9_-]+)/)?.[1]||t.getAttribute("data-id")||t.getAttribute("data-track-id")||"";if(!n)continue;const m=t.querySelector('a[href^="/artist/"]')||t.querySelector(".track__artist")||t.querySelector(".artist"),u=i(r.textContent),h=i(m?.textContent)||"Unknown",s=(t.querySelector(".track__time")?.textContent?.trim()||t.querySelector(".duration")?.textContent?.trim()||t.querySelector("time")?.textContent?.trim()||"").split(":"),l=s.length===2?parseInt(s[0],10)*60+parseInt(s[1],10):0;e.push({id:n,title:u,artist:h,duration:l,uri:`hitmoz:track:${n}`,name:u,duration_ms:l*1e3,explicit:!1})}return e}parseTrackPage(o,e,a){const t=i(o.querySelector(".track__title, h1, .title")?.textContent)||"Unknown",r=i(o.querySelector(".track__artist, .artist, a[href^='/artist/']")?.textContent)||"Unknown";return{title:t,artist:r}}extractAudioUrl(o,e,a,t){const r=e.match(/(https?:\/\/[^\s"'<>]+\.mp3[^\s"'<>]*)/);if(r?.[1])return r[1].replace(/\\/g,"").trim();const c=e.match(/"url"\s*:\s*"([^"]+)"/);if(c?.[1])return c[1].replace(/\\/g,"").trim();const n=e.match(/["'](\/L[a-zA-Z0-9_=]+\.mp3)["']/);return n?.[1]?`https://pl1.hitmos.fm${n[1]}`.replace(/\\/g,"").trim():null}}
2
+ //# sourceMappingURL=hitmoz.provider.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/providers/hitmoz.provider.ts"],
4
+ "sourcesContent": ["import type { HTMLElement } from \"node-html-parser\";\nimport type { SiteProvider } from \"./base.js\";\nimport type { TrackMeta } from \"../models/track.js\";\nimport { normalizeText } from \"../core/normalize.js\";\n\nexport class HitmozProvider implements SiteProvider {\n readonly name = \"hitmoz\";\n\n readonly hosts = [\"rus.hitmoz.org\", \"hitmoz.org\", \"www.hitmoz.org\"];\n\n matches(host: string, html?: string): boolean {\n return host.includes(\"hitmoz\") || html?.includes(\"tracks__item\") === true;\n }\n\n parseSearch(root: HTMLElement): TrackMeta[] {\n const results: TrackMeta[] = [];\n const items = root.querySelectorAll(\"li.tracks__item, .tracks__item, li.relative\");\n\n for (const item of items) {\n const songLink =\n item.querySelector('a[href^=\"/song/\"]') ||\n item.querySelector(\".track__title a\") ||\n item.querySelector(\".track__title\");\n\n if (!songLink) continue;\n\n const href = songLink.getAttribute?.(\"href\") || \"\";\n const id =\n href.match(/\\/song\\/([a-zA-Z0-9_-]+)/)?.[1] ||\n item.getAttribute(\"data-id\") ||\n item.getAttribute(\"data-track-id\") ||\n \"\";\n\n if (!id) continue;\n\n const artistLink =\n item.querySelector('a[href^=\"/artist/\"]') ||\n item.querySelector(\".track__artist\") ||\n item.querySelector(\".artist\");\n\n const title = normalizeText(songLink.textContent);\n const artist = normalizeText(artistLink?.textContent) || \"Unknown\";\n\n const durationText =\n item.querySelector(\".track__time\")?.textContent?.trim() ||\n item.querySelector(\".duration\")?.textContent?.trim() ||\n item.querySelector(\"time\")?.textContent?.trim() ||\n \"\";\n\n const parts = durationText.split(\":\");\n const duration =\n parts.length === 2 ? parseInt(parts[0]!, 10) * 60 + parseInt(parts[1]!, 10) : 0;\n\n results.push({\n id,\n title,\n artist,\n duration,\n uri: `hitmoz:track:${id}`,\n name: title,\n duration_ms: duration * 1000,\n explicit: false,\n });\n }\n\n return results;\n }\n\n parseTrackPage(root: HTMLElement, _host: string, _trackId: string): Partial<TrackMeta> {\n const title =\n normalizeText(root.querySelector(\".track__title, h1, .title\")?.textContent) || \"Unknown\";\n const artist =\n normalizeText(\n root.querySelector(\".track__artist, .artist, a[href^='/artist/']\")?.textContent,\n ) || \"Unknown\";\n\n return { title, artist };\n }\n\n extractAudioUrl(\n _root: HTMLElement,\n html: string,\n _host: string,\n _trackId: string,\n ): string | null {\n const directMatch = html.match(/(https?:\\/\\/[^\\s\"'<>]+\\.mp3[^\\s\"'<>]*)/);\n if (directMatch?.[1]) return directMatch[1].replace(/\\\\/g, \"\").trim();\n\n const jsonMatch = html.match(/\"url\"\\s*:\\s*\"([^\"]+)\"/);\n if (jsonMatch?.[1]) return jsonMatch[1].replace(/\\\\/g, \"\").trim();\n\n const base64Match = html.match(/[\"'](\\/L[a-zA-Z0-9_=]+\\.mp3)[\"']/);\n if (base64Match?.[1]) return `https://pl1.hitmos.fm${base64Match[1]}`.replace(/\\\\/g, \"\").trim();\n\n return null;\n }\n}\n"],
5
+ "mappings": "aAGA,OAAS,iBAAAA,MAAqB,uBAExB,aAAO,cAAc,CAChB,KAAO,SAEP,MAAQ,CAAC,iBAAkB,aAAc,gBAAgB,EAElE,QAAQC,EAAcC,EAAa,CACjC,OAAOD,EAAK,SAAS,QAAQ,GAAKC,GAAM,SAAS,cAAc,IAAM,EACvE,CAEA,YAAYC,EAAiB,CAC3B,MAAMC,EAAuB,CAAA,EACvBC,EAAQF,EAAK,iBAAiB,6CAA6C,EAEjF,UAAWG,KAAQD,EAAO,CACxB,MAAME,EACJD,EAAK,cAAc,mBAAmB,GACtCA,EAAK,cAAc,iBAAiB,GACpCA,EAAK,cAAc,eAAe,EAEpC,GAAI,CAACC,EAAU,SAGf,MAAMC,GADOD,EAAS,eAAe,MAAM,GAAK,IAEzC,MAAM,0BAA0B,IAAI,CAAC,GAC1CD,EAAK,aAAa,SAAS,GAC3BA,EAAK,aAAa,eAAe,GACjC,GAEF,GAAI,CAACE,EAAI,SAET,MAAMC,EACJH,EAAK,cAAc,qBAAqB,GACxCA,EAAK,cAAc,gBAAgB,GACnCA,EAAK,cAAc,SAAS,EAExBI,EAAQV,EAAcO,EAAS,WAAW,EAC1CI,EAASX,EAAcS,GAAY,WAAW,GAAK,UAQnDG,GALJN,EAAK,cAAc,cAAc,GAAG,aAAa,KAAI,GACrDA,EAAK,cAAc,WAAW,GAAG,aAAa,KAAI,GAClDA,EAAK,cAAc,MAAM,GAAG,aAAa,KAAI,GAC7C,IAEyB,MAAM,GAAG,EAC9BO,EACJD,EAAM,SAAW,EAAI,SAASA,EAAM,CAAC,EAAI,EAAE,EAAI,GAAK,SAASA,EAAM,CAAC,EAAI,EAAE,EAAI,EAEhFR,EAAQ,KAAK,CACX,GAAAI,EACA,MAAAE,EACA,OAAAC,EACA,SAAAE,EACA,IAAK,gBAAgBL,CAAE,GACvB,KAAME,EACN,YAAaG,EAAW,IACxB,SAAU,GACX,CACH,CAEA,OAAOT,CACT,CAEA,eAAeD,EAAmBW,EAAeC,EAAgB,CAC/D,MAAML,EACJV,EAAcG,EAAK,cAAc,2BAA2B,GAAG,WAAW,GAAK,UAC3EQ,EACJX,EACEG,EAAK,cAAc,8CAA8C,GAAG,WAAW,GAC5E,UAEP,MAAO,CAAE,MAAAO,EAAO,OAAAC,CAAM,CACxB,CAEA,gBACEK,EACAd,EACAY,EACAC,EAAgB,CAEhB,MAAME,EAAcf,EAAK,MAAM,wCAAwC,EACvE,GAAIe,IAAc,CAAC,EAAG,OAAOA,EAAY,CAAC,EAAE,QAAQ,MAAO,EAAE,EAAE,KAAI,EAEnE,MAAMC,EAAYhB,EAAK,MAAM,uBAAuB,EACpD,GAAIgB,IAAY,CAAC,EAAG,OAAOA,EAAU,CAAC,EAAE,QAAQ,MAAO,EAAE,EAAE,KAAI,EAE/D,MAAMC,EAAcjB,EAAK,MAAM,kCAAkC,EACjE,OAAIiB,IAAc,CAAC,EAAU,wBAAwBA,EAAY,CAAC,CAAC,GAAG,QAAQ,MAAO,EAAE,EAAE,KAAI,EAEtF,IACT",
6
+ "names": ["normalizeText", "host", "html", "root", "results", "items", "item", "songLink", "id", "artistLink", "title", "artist", "parts", "duration", "_host", "_trackId", "_root", "directMatch", "jsonMatch", "base64Match"]
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { SiteProvider } from "./base.js";
2
+ export declare class ProviderRegistry {
3
+ private readonly providers;
4
+ constructor(providers: SiteProvider[]);
5
+ resolve(host: string, html?: string): SiteProvider;
6
+ }
7
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,YAAY,EAAE;IAEtD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY;CAOnD"}
@@ -0,0 +1,2 @@
1
+ "use strict";export class ProviderRegistry{providers;constructor(r){this.providers=r}resolve(r,i){return this.providers.find(s=>s.hosts.includes(r))??this.providers.find(s=>s.matches(r,i))??this.providers[0]}}
2
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/providers/registry.ts"],
4
+ "sourcesContent": ["import type { SiteProvider } from \"./base.js\";\n\nexport class ProviderRegistry {\n constructor(private readonly providers: SiteProvider[]) {}\n\n resolve(host: string, html?: string): SiteProvider {\n return (\n this.providers.find((p) => p.hosts.includes(host)) ??\n this.providers.find((p) => p.matches(host, html)) ??\n this.providers[0]!\n );\n }\n}\n"],
5
+ "mappings": "aAEM,aAAO,gBAAgB,CACE,UAA7B,YAA6BA,EAAyB,CAAzB,KAAA,UAAAA,CAA4B,CAEzD,QAAQC,EAAcC,EAAa,CACjC,OACE,KAAK,UAAU,KAAMC,GAAMA,EAAE,MAAM,SAASF,CAAI,CAAC,GACjD,KAAK,UAAU,KAAME,GAAMA,EAAE,QAAQF,EAAMC,CAAI,CAAC,GAChD,KAAK,UAAU,CAAC,CAEpB",
6
+ "names": ["providers", "host", "html", "p"]
7
+ }