Skip to content

Commit 73abdf3

Browse files
committed
add: implement meanings functionality with letrasmus source support
1 parent 602a747 commit 73abdf3

File tree

7 files changed

+425
-177
lines changed

7 files changed

+425
-177
lines changed

config.default.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ export default {
384384
enabled: true
385385
}
386386
},
387+
meanings: {
388+
letrasmus: {
389+
enabled: true
390+
}
391+
},
387392
audio: {
388393
quality: 'high', // high, medium, low, lowest
389394
encryption: 'aead_aes256_gcm_rtpsize',

src/api/meaning.js

Lines changed: 39 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,11 @@
11
import myzod from 'myzod'
2-
import { translateMany, translateText } from '../modules/googleTranslate.js'
3-
import {
4-
decodeTrack,
5-
http1makeRequest,
6-
logger,
7-
sendErrorResponse
8-
} from '../utils.js'
2+
import { decodeTrack, logger, sendErrorResponse } from '../utils.js'
93

104
const meaningSchema = myzod.object({
115
encodedTrack: myzod.string(),
126
lang: myzod.string().optional()
137
})
148

15-
const decodeHtml = (text) => {
16-
if (!text) return text
17-
return text
18-
.replace(/&/g, '&')
19-
.replace(/"/g, '"')
20-
.replace(/'/g, "'")
21-
.replace(/'/g, "'")
22-
.replace(/&lt;/g, '<')
23-
.replace(/&gt;/g, '>')
24-
}
25-
26-
const extractMeta = (html, property) => {
27-
const re1 = new RegExp(
28-
`<meta[^>]+property=[\"']${property}[\"'][^>]+content=[\"']([^\"']+)[\"'][^>]*>`,
29-
'i'
30-
)
31-
const re2 = new RegExp(
32-
`<meta[^>]+content=[\"']([^\"']+)[^>]+property=[\"']${property}[\"'][^>]*>`,
33-
'i'
34-
)
35-
const match = html.match(re1) || html.match(re2)
36-
return match ? decodeHtml(match[1]) : null
37-
}
38-
39-
const extractOmqLyric = (html) => {
40-
const match = html.match(/_omq\.push\(\['ui\/lyric',\s*({[\s\S]*?})\s*,/i)
41-
if (!match) return null
42-
try {
43-
return JSON.parse(match[1])
44-
} catch {
45-
return null
46-
}
47-
}
48-
49-
const extractOmqMeaning = (html) => {
50-
const match = html.match(
51-
/_omq\.push\(\['ui\/lyric',\s*({[\s\S]*?})\s*,\s*({[\s\S]*?})\s*,/i
52-
)
53-
if (!match) return null
54-
try {
55-
return JSON.parse(match[2])
56-
} catch {
57-
return null
58-
}
59-
}
60-
61-
const extractMeaning = (html) => {
62-
const match = html.match(/<div class="lyric-meaning[^>]*">([\s\S]*?)<\/div>/i)
63-
if (!match) return { title: null, body: [] }
64-
let block = match[1]
65-
const titleMatch = block.match(/<h3[^>]*>([\s\S]*?)<\/h3>/i)
66-
const title = titleMatch ? decodeHtml(titleMatch[1].replace(/<[^>]+>/g, '')) : null
67-
block = block.replace(/<h3[^>]*>[\s\S]*?<\/h3>/i, '')
68-
const paragraphs = []
69-
const pRegex = /<p[^>]*>([\s\S]*?)<\/p>/gi
70-
let pMatch
71-
while ((pMatch = pRegex.exec(block))) {
72-
let text = pMatch[1]
73-
text = text.replace(/<br\s*\/?>/gi, '\n')
74-
text = text.replace(/<[^>]+>/g, '')
75-
text = decodeHtml(text)
76-
const lines = text
77-
.split('\n')
78-
.map((line) => line.trim())
79-
.filter(Boolean)
80-
if (lines.length) paragraphs.push(lines.join(' '))
81-
}
82-
if (!paragraphs.length) {
83-
let text = block.replace(/<br\s*\/?>/gi, '\n')
84-
text = text.replace(/<[^>]+>/g, '')
85-
text = decodeHtml(text)
86-
const lines = text
87-
.split('\n')
88-
.map((line) => line.trim())
89-
.filter(Boolean)
90-
if (lines.length) paragraphs.push(lines.join(' '))
91-
}
92-
return { title, body: paragraphs }
93-
}
94-
959
async function handler(nodelink, req, res, sendResponse, parsedUrl) {
9610
const result = meaningSchema.try({
9711
encodedTrack: parsedUrl.searchParams.get('encodedTrack'),
@@ -130,106 +44,56 @@ async function handler(nodelink, req, res, sendResponse, parsedUrl) {
13044
}
13145

13246
try {
133-
let trackInfo = decodedTrack.info
134-
if (nodelink.sources?.resolve && decodedTrack.info?.uri) {
135-
const resolved = await nodelink.sources.resolve(decodedTrack.info.uri)
136-
if (resolved.loadType !== 'track') {
137-
return sendResponse(
138-
req,
139-
res,
140-
{ loadType: 'empty', data: {} },
141-
200
142-
)
143-
}
144-
trackInfo = resolved.data?.info || resolved.data
145-
}
146-
147-
if (!trackInfo || trackInfo.sourceName !== 'letrasmus') {
148-
return sendResponse(
47+
let delegated = false
48+
if (nodelink.sourceWorkerManager) {
49+
delegated = nodelink.sourceWorkerManager.delegate(
14950
req,
15051
res,
151-
{ loadType: 'empty', data: {} },
152-
200
52+
'loadMeaning',
53+
{
54+
decodedTrackInfo: decodedTrack.info,
55+
language: targetLang
56+
}
15357
)
15458
}
15559

156-
const baseUrl = trackInfo.uri?.endsWith('/')
157-
? trackInfo.uri
158-
: `${trackInfo.uri}/`
159-
const meaningUrl = `${baseUrl}significado.html`
160-
const { body, statusCode, error } = await http1makeRequest(meaningUrl, {
161-
method: 'GET'
162-
}).catch((e) => ({ error: e }))
163-
164-
if (error || statusCode !== 200 || !body) {
165-
return sendResponse(
60+
if (delegated) return
61+
62+
let meaning
63+
if (nodelink.workerManager) {
64+
const worker = nodelink.workerManager.getBestWorker()
65+
meaning = await nodelink.workerManager.execute(worker, 'loadMeaning', {
66+
decodedTrackInfo: decodedTrack.info,
67+
language: targetLang
68+
})
69+
} else if (nodelink.meanings?.loadMeaning) {
70+
meaning = await nodelink.meanings.loadMeaning(decodedTrack, targetLang)
71+
} else {
72+
logger('error', 'Meaning', 'Meaning sources are not available.')
73+
return sendErrorResponse(
16674
req,
16775
res,
168-
{ loadType: 'empty', data: {} },
169-
200
76+
503,
77+
'meaning sources unavailable',
78+
'Meaning sources are not available.',
79+
parsedUrl.pathname,
80+
true
17081
)
17182
}
17283

173-
const meaning = extractMeaning(body)
174-
const omq = extractOmqLyric(body)
175-
const meaningMeta = extractOmqMeaning(body)
176-
const ogImage = extractMeta(body, 'og:image')
177-
const ogTitle = extractMeta(body, 'og:title')
178-
const ogDescription = extractMeta(body, 'og:description')
179-
180-
let translated = null
181-
if (targetLang) {
182-
const sourceLang = 'pt'
183-
try {
184-
const translatedParagraphs = await translateMany(
185-
meaning.body,
186-
sourceLang,
187-
targetLang
188-
)
189-
const translatedTitle = meaning.title
190-
? await translateText(meaning.title, sourceLang, targetLang)
191-
: null
192-
const translatedDescription = ogDescription
193-
? await translateText(ogDescription, sourceLang, targetLang)
194-
: null
195-
translated = {
196-
language: {
197-
source: sourceLang,
198-
target: targetLang
199-
},
200-
title: translatedTitle?.translation || null,
201-
description: translatedDescription?.translation || null,
202-
paragraphs: translatedParagraphs
203-
}
204-
} catch (e) {
205-
logger('warn', 'Meaning', `Translate failed: ${e.message}`)
206-
}
84+
if (meaning?.loadType === 'error') {
85+
return sendErrorResponse(
86+
req,
87+
res,
88+
500,
89+
'failed to load meaning',
90+
meaning.data?.message || 'Failed to load meaning',
91+
parsedUrl.pathname,
92+
true
93+
)
20794
}
20895

209-
return sendResponse(req, res, {
210-
loadType: meaning.body.length ? 'meaning' : 'empty',
211-
data: {
212-
title: meaning.title || ogTitle || null,
213-
description: ogDescription || null,
214-
paragraphs: meaning.body,
215-
translation: translated,
216-
url: meaningUrl,
217-
meaningMeta: {
218-
id: meaningMeta?.ID || null,
219-
localeId: meaningMeta?.LocaleID || null,
220-
origin: meaningMeta?.Origin || null,
221-
submittedBy: null,
222-
reviewedBy: null
223-
},
224-
song: {
225-
title: omq?.Name || trackInfo.title || null,
226-
artist: omq?.Artist || trackInfo.author || null,
227-
youtubeId: omq?.YoutubeID || null,
228-
letrasId: omq?.ID || null,
229-
artworkUrl: ogImage || trackInfo.artworkUrl || null
230-
}
231-
}
232-
}, 200)
96+
return sendResponse(req, res, meaning, 200)
23397
} catch (err) {
23498
logger('error', 'Meaning', `Failed to load meaning: ${err.message}`)
23599
return sendErrorResponse(

src/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class NodelinkServer extends EventEmitter {
163163
this.sessions = new sessionManager(this, PlayerManagerClass)
164164
this.sources = null
165165
this.lyrics = null
166+
this.meanings = null
166167

167168
this._sourceInitPromise = this._initSources(isClusterPrimary, options)
168169

@@ -226,13 +227,15 @@ class NodelinkServer extends EventEmitter {
226227

227228
async _initSources(isClusterPrimary, _options) {
228229
if (!isClusterPrimary) {
229-
const [{ default: sourceMan }, { default: lyricsMan }] =
230+
const [{ default: sourceMan }, { default: lyricsMan }, { default: meaningMan }] =
230231
await Promise.all([
231232
import('./managers/sourceManager.js'),
232-
import('./managers/lyricsManager.js')
233+
import('./managers/lyricsManager.js'),
234+
import('./managers/meaningManager.js')
233235
])
234236
this.sources = new sourceMan(this)
235237
this.lyrics = new lyricsMan(this)
238+
this.meanings = new meaningMan(this)
236239
}
237240
}
238241

@@ -1543,6 +1546,7 @@ class NodelinkServer extends EventEmitter {
15431546
if (this.sources && (!startOptions.isClusterPrimary || !specEnabled)) {
15441547
await this.sources.loadFolder()
15451548
await this.lyrics.loadFolder()
1549+
await this.meanings.loadFolder()
15461550
}
15471551

15481552
this._setupSocketEvents()

0 commit comments

Comments
 (0)