lyricwiki-qt.cc
1 |
/*
|
---|---|
2 |
* Copyright (c) 2010, 2014 William Pitcock <nenolod@dereferenced.org>
|
3 |
*
|
4 |
* Permission to use, copy, modify, and/or distribute this software for any
|
5 |
* purpose with or without fee is hereby granted, provided that the above
|
6 |
* copyright notice and this permission notice appear in all copies.
|
7 |
*
|
8 |
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
9 |
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
10 |
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
11 |
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
12 |
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
13 |
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
14 |
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
15 |
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
16 |
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
17 |
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
18 |
* POSSIBILITY OF SUCH DAMAGE.
|
19 |
*/
|
20 |
|
21 |
#include <glib.h> |
22 |
#include <glib/gstdio.h> |
23 |
#include <string.h> |
24 |
|
25 |
#include <QApplication> |
26 |
#include <QContextMenuEvent> |
27 |
#include <QDesktopServices> |
28 |
#include <QMenu> |
29 |
#include <QTextCursor> |
30 |
#include <QTextDocument> |
31 |
#include <QTextEdit> |
32 |
|
33 |
#include <libxml/parser.h> |
34 |
#include <libxml/tree.h> |
35 |
#include <libxml/HTMLparser.h> |
36 |
#include <libxml/xpath.h> |
37 |
|
38 |
#define AUD_GLIB_INTEGRATION
|
39 |
#include <libaudcore/drct.h> |
40 |
#include <libaudcore/i18n.h> |
41 |
#include <libaudcore/plugin.h> |
42 |
#include <libaudcore/plugins.h> |
43 |
#include <libaudcore/audstrings.h> |
44 |
#include <libaudcore/hook.h> |
45 |
#include <libaudcore/vfs_async.h> |
46 |
#include <libaudcore/runtime.h> |
47 |
|
48 |
#include <libaudqt/libaudqt.h> |
49 |
|
50 |
typedef struct { |
51 |
String filename; /* of song file */
|
52 |
String title, artist; |
53 |
String uri; /* URI we are trying to retrieve */
|
54 |
String local_filename; /* JWT:CALCULATED LOCAL FILENAME TO SAVE LYRICS TO */
|
55 |
int startlyrics; /* JWT:OFFSET IN LYRICS WINDOW WHERE LYRIC TEXT ACTUALLY STARTS */ |
56 |
bool ok2save; /* JWT:SET TO TRUE IF GOT LYRICS FROM LYRICWIKI (LOCAL FILE DOESN'T EXIST) */ |
57 |
} LyricsState; |
58 |
|
59 |
static LyricsState state;
|
60 |
|
61 |
class TextEdit : public QTextEdit |
62 |
{ |
63 |
public:
|
64 |
TextEdit (QWidget * parent = nullptr) : QTextEdit (parent) {}
|
65 |
|
66 |
protected:
|
67 |
void contextMenuEvent (QContextMenuEvent * event);
|
68 |
}; |
69 |
|
70 |
class LyricWikiQt : public GeneralPlugin |
71 |
{ |
72 |
public:
|
73 |
static constexpr PluginInfo info = { |
74 |
N_("LyricWiki Plugin"),
|
75 |
PACKAGE, |
76 |
nullptr, // about |
77 |
nullptr, // prefs |
78 |
PluginQtOnly |
79 |
}; |
80 |
|
81 |
constexpr LyricWikiQt () : GeneralPlugin (info, false) {} |
82 |
void * get_qt_widget ();
|
83 |
}; |
84 |
|
85 |
EXPORT LyricWikiQt aud_plugin_instance; |
86 |
|
87 |
/*
|
88 |
* Suppress libxml warnings, because lyricwiki does not generate anything near
|
89 |
* valid HTML.
|
90 |
*/
|
91 |
static void libxml_error_handler (void * ctx, const char * msg, ...) |
92 |
{ |
93 |
} |
94 |
|
95 |
static CharPtr scrape_lyrics_from_lyricwiki_edit_page (const char * buf, int64_t len) |
96 |
{ |
97 |
xmlDocPtr doc; |
98 |
CharPtr ret; |
99 |
|
100 |
/*
|
101 |
* temporarily set our error-handling functor to our suppression function,
|
102 |
* but we have to set it back because other components of Audacious depend
|
103 |
* on libxml and we don't want to step on their code paths.
|
104 |
*
|
105 |
* unfortunately, libxml is anti-social and provides us with no way to get
|
106 |
* the previous error functor, so we just have to set it back to default after
|
107 |
* parsing and hope for the best.
|
108 |
*/
|
109 |
xmlSetGenericErrorFunc (nullptr, libxml_error_handler);
|
110 |
doc = htmlReadMemory (buf, (int) len, nullptr, "utf-8", (HTML_PARSE_RECOVER | HTML_PARSE_NONET)); |
111 |
xmlSetGenericErrorFunc (nullptr, nullptr); |
112 |
|
113 |
if (doc)
|
114 |
{ |
115 |
xmlXPathContextPtr xpath_ctx = nullptr;
|
116 |
xmlXPathObjectPtr xpath_obj = nullptr;
|
117 |
xmlNodePtr node = nullptr;
|
118 |
|
119 |
xpath_ctx = xmlXPathNewContext (doc); |
120 |
if (! xpath_ctx)
|
121 |
goto give_up;
|
122 |
|
123 |
xpath_obj = xmlXPathEvalExpression ((xmlChar *) "//*[@id=\"wpTextbox1\"]", xpath_ctx);
|
124 |
if (! xpath_obj)
|
125 |
goto give_up;
|
126 |
|
127 |
if (! xpath_obj->nodesetval->nodeMax)
|
128 |
goto give_up;
|
129 |
|
130 |
node = xpath_obj->nodesetval->nodeTab[0];
|
131 |
give_up:
|
132 |
if (xpath_obj)
|
133 |
xmlXPathFreeObject (xpath_obj); |
134 |
|
135 |
if (xpath_ctx)
|
136 |
xmlXPathFreeContext (xpath_ctx); |
137 |
|
138 |
if (node)
|
139 |
{ |
140 |
xmlChar * lyric = xmlNodeGetContent (node); |
141 |
|
142 |
if (lyric)
|
143 |
{ |
144 |
GMatchInfo * match_info; |
145 |
GRegex * reg; |
146 |
|
147 |
reg = g_regex_new |
148 |
("<(lyrics?)>[[:space:]]*(.*?)[[:space:]]*</\\1>",
|
149 |
(GRegexCompileFlags) (G_REGEX_MULTILINE | G_REGEX_DOTALL), |
150 |
(GRegexMatchFlags) 0, nullptr); |
151 |
g_regex_match (reg, (char *) lyric, G_REGEX_MATCH_NEWLINE_ANY, & match_info);
|
152 |
|
153 |
ret.capture (g_match_info_fetch (match_info, 2));
|
154 |
if (! strcmp_nocase (ret, "<!-- PUT LYRICS HERE (and delete this entire line) -->")) |
155 |
ret.capture (g_strdup (_("No lyrics available")));
|
156 |
|
157 |
g_match_info_free (match_info); |
158 |
g_regex_unref (reg); |
159 |
|
160 |
if (! ret)
|
161 |
{ |
162 |
reg = g_regex_new |
163 |
("#REDIRECT \\[\\[([^:]*):(.*)]]",
|
164 |
(GRegexCompileFlags) (G_REGEX_MULTILINE | G_REGEX_DOTALL), |
165 |
(GRegexMatchFlags) 0, nullptr); |
166 |
if (g_regex_match (reg, (char *) lyric, G_REGEX_MATCH_NEWLINE_ANY, & match_info)) |
167 |
{ |
168 |
state.artist = String (g_match_info_fetch (match_info, 1));
|
169 |
state.title = String (g_match_info_fetch (match_info, 2));
|
170 |
state.uri = String (); |
171 |
} |
172 |
|
173 |
g_match_info_free (match_info); |
174 |
g_regex_unref (reg); |
175 |
} |
176 |
} |
177 |
|
178 |
xmlFree (lyric); |
179 |
} |
180 |
|
181 |
xmlFreeDoc (doc); |
182 |
} |
183 |
|
184 |
return ret;
|
185 |
} |
186 |
|
187 |
static String scrape_uri_from_lyricwiki_search_result (const char * buf, int64_t len) |
188 |
{ |
189 |
xmlDocPtr doc; |
190 |
String uri; |
191 |
|
192 |
/*
|
193 |
* workaround buggy lyricwiki search output where it cuts the lyrics
|
194 |
* halfway through the UTF-8 symbol resulting in invalid XML.
|
195 |
*/
|
196 |
GRegex * reg; |
197 |
|
198 |
reg = g_regex_new ("<(lyrics?)>.*</\\1>", (GRegexCompileFlags)
|
199 |
(G_REGEX_MULTILINE | G_REGEX_DOTALL | G_REGEX_UNGREEDY), |
200 |
(GRegexMatchFlags) 0, nullptr); |
201 |
CharPtr newbuf (g_regex_replace_literal (reg, buf, len, 0, "", G_REGEX_MATCH_NEWLINE_ANY, nullptr)); |
202 |
g_regex_unref (reg); |
203 |
|
204 |
/*
|
205 |
* temporarily set our error-handling functor to our suppression function,
|
206 |
* but we have to set it back because other components of Audacious depend
|
207 |
* on libxml and we don't want to step on their code paths.
|
208 |
*
|
209 |
* unfortunately, libxml is anti-social and provides us with no way to get
|
210 |
* the previous error functor, so we just have to set it back to default after
|
211 |
* parsing and hope for the best.
|
212 |
*/
|
213 |
xmlSetGenericErrorFunc (nullptr, libxml_error_handler);
|
214 |
doc = xmlParseMemory (newbuf, strlen (newbuf)); |
215 |
xmlSetGenericErrorFunc (nullptr, nullptr); |
216 |
|
217 |
if (doc != nullptr) |
218 |
{ |
219 |
xmlNodePtr root, cur; |
220 |
|
221 |
root = xmlDocGetRootElement(doc); |
222 |
|
223 |
for (cur = root->xmlChildrenNode; cur; cur = cur->next)
|
224 |
{ |
225 |
if (xmlStrEqual(cur->name, (xmlChar *) "url")) |
226 |
{ |
227 |
auto lyric = (char *) xmlNodeGetContent (cur); |
228 |
|
229 |
// If the lyrics are unknown, LyricWiki returns a broken link
|
230 |
// to the edit page. Extract the song ID (artist:title) from
|
231 |
// the URI and recreate a working link.
|
232 |
char * title = strstr (lyric, "title="); |
233 |
if (title)
|
234 |
{ |
235 |
title += 6;
|
236 |
|
237 |
// Strip trailing "&action=edit"
|
238 |
char * amp = strchr (title, '&'); |
239 |
if (amp)
|
240 |
* amp = 0;
|
241 |
|
242 |
// Spaces get replaced with plus signs for some reason.
|
243 |
str_replace_char (title, '+', ' '); |
244 |
|
245 |
// LyricWiki interprets UTF-8 as ISO-8859-1, then "converts"
|
246 |
// it to UTF-8 again. Worse, it will sometimes corrupt only
|
247 |
// the song title in this way while leaving the artist name
|
248 |
// intact. So we have to percent-decode the URI, split the
|
249 |
// two strings apart, repair them separately, and then
|
250 |
// rebuild the URI.
|
251 |
auto strings = str_list_to_index (str_decode_percent (title), ":"); |
252 |
for (String & s : strings)
|
253 |
{ |
254 |
StringBuf orig_utf8 = str_convert (s, -1, "UTF-8", "ISO-8859-1"); |
255 |
if (orig_utf8 && g_utf8_validate (orig_utf8, -1, nullptr)) |
256 |
s = String (orig_utf8); |
257 |
} |
258 |
|
259 |
uri = String (str_printf ("https://lyrics.fandom.com/index.php?"
|
260 |
"action=edit&title=%s", (const char *) str_encode_percent |
261 |
(index_to_str_list (strings, ":"))));
|
262 |
} |
263 |
else
|
264 |
{ |
265 |
// Convert normal lyrics link to edit page link
|
266 |
char * slash = strrchr (lyric, '/'); |
267 |
// if (slash && ! strstr (slash, "lyrics.wikia.com")) // JWT:FIXME?! - CHGD. TO MATCH GTK SIDE!:
|
268 |
if (slash))
|
269 |
uri = String (str_printf ("https://lyrics.fandom.com/index.php?"
|
270 |
"action=edit&title=%s", slash + 1)); |
271 |
else
|
272 |
uri = String ("N/A");
|
273 |
} |
274 |
|
275 |
xmlFree ((xmlChar *) lyric); |
276 |
} |
277 |
} |
278 |
|
279 |
xmlFreeDoc (doc); |
280 |
} |
281 |
|
282 |
return uri;
|
283 |
} |
284 |
|
285 |
static void update_lyrics_window (const char * title, const char * artist, const char * lyrics); |
286 |
|
287 |
static void get_lyrics_step_1 (); |
288 |
|
289 |
static void get_lyrics_step_3 (const char * uri, const Index<char> & buf, void *) |
290 |
{ |
291 |
if (! state.uri || strcmp (state.uri, uri))
|
292 |
return;
|
293 |
|
294 |
if (! buf.len ())
|
295 |
{ |
296 |
update_lyrics_window (_("Error"), nullptr, |
297 |
str_printf (_("Unable to fetch %s"), uri));
|
298 |
return;
|
299 |
} |
300 |
|
301 |
CharPtr lyrics = scrape_lyrics_from_lyricwiki_edit_page (buf.begin (), buf.len ()); |
302 |
|
303 |
if (! state.uri)
|
304 |
{ |
305 |
get_lyrics_step_1 (); |
306 |
return;
|
307 |
} |
308 |
|
309 |
if (! lyrics)
|
310 |
{ |
311 |
update_lyrics_window (_("No lyrics Found"),
|
312 |
(const char *) str_concat ({"Title: ", (const char *) state.title, "\nArtist: ", |
313 |
(const char *) state.artist}), |
314 |
str_printf (_("Unable to parse %s"), uri));
|
315 |
return;
|
316 |
} |
317 |
|
318 |
update_lyrics_window (state.title, state.artist, lyrics); |
319 |
state.ok2save = true;
|
320 |
} |
321 |
|
322 |
static void get_lyrics_step_2 (const char * uri1, const Index<char> & buf, void *) |
323 |
{ |
324 |
if (! state.uri || strcmp (state.uri, uri1))
|
325 |
return;
|
326 |
|
327 |
if (! buf.len ())
|
328 |
{ |
329 |
update_lyrics_window (_("Error"), nullptr, |
330 |
str_printf (_("Unable to fetch %s"), uri1));
|
331 |
return;
|
332 |
} |
333 |
|
334 |
String uri = scrape_uri_from_lyricwiki_search_result (buf.begin (), buf.len ()); |
335 |
|
336 |
if (! uri)
|
337 |
{ |
338 |
update_lyrics_window (_("Error"), nullptr, |
339 |
str_printf (_("Unable to parse %s"), uri1));
|
340 |
return;
|
341 |
} |
342 |
else if (uri == String ("N/A")) |
343 |
{ |
344 |
update_lyrics_window (state.title, state.artist, |
345 |
_("No lyrics available"));
|
346 |
return;
|
347 |
} |
348 |
|
349 |
state.uri = uri; |
350 |
|
351 |
update_lyrics_window (state.title, state.artist, _("Looking for lyrics ..."));
|
352 |
vfs_async_file_get_contents (uri, get_lyrics_step_3, nullptr);
|
353 |
} |
354 |
|
355 |
static void get_lyrics_step_1 () |
356 |
{ |
357 |
if (! state.artist || ! state.title)
|
358 |
{ |
359 |
update_lyrics_window (_("Error"), nullptr, _("Missing title and/or artist")); |
360 |
return;
|
361 |
} |
362 |
|
363 |
StringBuf title_buf = str_encode_percent (state.title); |
364 |
StringBuf artist_buf = str_encode_percent (state.artist); |
365 |
|
366 |
state.uri = String (str_printf ("https://lyrics.fandom.com/api.php?"
|
367 |
"action=lyrics&artist=%s&song=%s&fmt=xml", (const char *) artist_buf, |
368 |
(const char *) title_buf)); |
369 |
|
370 |
update_lyrics_window (state.title, state.artist, _("Connecting to lyrics.fandom.com ..."));
|
371 |
vfs_async_file_get_contents (state.uri, get_lyrics_step_2, nullptr);
|
372 |
} |
373 |
|
374 |
static void get_lyrics_step_0 (const char * uri, const Index<char> & buf, void *) |
375 |
{ |
376 |
if (! buf.len ())
|
377 |
{ |
378 |
update_lyrics_window (_("Error"), nullptr, |
379 |
str_printf (_("Unable to fetch file %s"), uri));
|
380 |
return;
|
381 |
} |
382 |
|
383 |
StringBuf nullterminated_buf = str_copy (buf.begin (), buf.len ()); |
384 |
update_lyrics_window (state.title, state.artist, (const char *) nullterminated_buf); |
385 |
|
386 |
/* JWT:ALLOW 'EM TO EDIT LYRICWIKI, EVEN IF LYRICS ARE LOCAL, IF THEY HAVE BOTH REQUIRED FIELDS: */
|
387 |
if (state.artist && state.title)
|
388 |
{ |
389 |
StringBuf title_buf = str_copy (state.title); |
390 |
str_replace_char (title_buf, ' ', '_'); |
391 |
title_buf = str_encode_percent (title_buf, -1);
|
392 |
StringBuf artist_buf = str_copy (state.artist); |
393 |
str_replace_char (artist_buf, ' ', '_'); |
394 |
artist_buf = str_encode_percent (artist_buf, -1);
|
395 |
state.uri = String (str_printf ("https://lyrics.fandom.com/index.php?action=edit&title=%s:%s",
|
396 |
(const char *) artist_buf, (const char *) title_buf)); |
397 |
} |
398 |
} |
399 |
|
400 |
static QTextEdit * textedit;
|
401 |
|
402 |
static void save_lyrics_locally () |
403 |
{ |
404 |
if (state.local_filename)
|
405 |
{ |
406 |
QString lyrics = textedit->toPlainText (); |
407 |
if (! lyrics.isNull() && ! lyrics.isEmpty())
|
408 |
{ |
409 |
if (state.startlyrics > 0) |
410 |
lyrics.remove(0, state.startlyrics);
|
411 |
|
412 |
int sz = lyrics.length ();
|
413 |
if (sz > 0) |
414 |
{ |
415 |
if (strstr ((const char *) state.local_filename, "/lyrics/")) |
416 |
{ |
417 |
GStatBuf statbuf; |
418 |
StringBuf path = filename_get_parent ((const char *) state.local_filename); |
419 |
if (g_stat ((const char *) path, & statbuf) |
420 |
&& g_mkdir ((const char *) path, 0755)) |
421 |
{ |
422 |
AUDERR ("e:Could not create missing lyrics directory (%s)!\n", (const char *) path); |
423 |
return;
|
424 |
} |
425 |
} |
426 |
VFSFile file (state.local_filename, "w");
|
427 |
if (file)
|
428 |
{ |
429 |
if (file.fwrite (lyrics.toUtf8().constData(), 1, sz) == sz) |
430 |
AUDINFO ("i:Successfully saved %d bytes of lyrics locally to (%s).\n", sz,
|
431 |
(const char *) state.local_filename); |
432 |
|
433 |
state.ok2save = false;
|
434 |
} |
435 |
} |
436 |
} |
437 |
} |
438 |
} |
439 |
|
440 |
static void update_lyrics_window (const char * title, const char * artist, const char * lyrics) |
441 |
{ |
442 |
if (! textedit)
|
443 |
return;
|
444 |
|
445 |
textedit->document ()->clear (); |
446 |
|
447 |
QTextCursor cursor (textedit->document ()); |
448 |
cursor.insertHtml (QString ("<big><b>") + QString (title) + QString ("</b></big>")); |
449 |
|
450 |
if (artist)
|
451 |
{ |
452 |
cursor.insertHtml (QString ("<br><i>") + QString (artist) + QString ("</i>")); |
453 |
} |
454 |
|
455 |
cursor.insertHtml ("<br><br>");
|
456 |
QString prelyrics = textedit->toPlainText (); |
457 |
state.startlyrics = prelyrics.length (); |
458 |
cursor.insertText (lyrics); |
459 |
} |
460 |
|
461 |
static void lyricwiki_playback_began () |
462 |
{ |
463 |
/* FIXME: cancel previous VFS requests (not possible with current API) */
|
464 |
|
465 |
bool found_lyricfile = false; |
466 |
GStatBuf statbuf; |
467 |
String lyricStr = String ("");
|
468 |
StringBuf path = StringBuf (); |
469 |
|
470 |
state.filename = aud_drct_get_filename (); |
471 |
state.uri = String (); |
472 |
|
473 |
state.local_filename = String ("");
|
474 |
if (! strncmp ((const char *) state.filename, "file://", 7)) // JWT:WE'RE A LOCAL FILE, CHECK FOR CORRESPONDING LYRICS FILE: |
475 |
{ |
476 |
/* JWT: EXTRACT JUST THE "NAME" PART TO USE TO NAME THE LYRICS FILE: */
|
477 |
const char * slash = state.filename ? strrchr (state.filename, '/') : nullptr; |
478 |
const char * base = slash ? slash + 1 : nullptr; |
479 |
|
480 |
if (base && base[0]) |
481 |
{ |
482 |
/* JWT:FIRST CHECK LOCAL DIRECTORY FOR A LYRICS FILE MATCHING FILE-NAME: */
|
483 |
const char * dot = strrchr (base, '.'); |
484 |
int ln = (dot && ! strstr (dot, ".cue?")) ? (dot - base) : -1; // SET TO FULL LENGTH(-1) IF NO EXTENSION OR NOT A CUESHEET. |
485 |
path = filename_get_parent (uri_to_filename (state.filename)); |
486 |
lyricStr = String (str_concat ({path, "/", str_decode_percent (base, ln), ".lrc"})); |
487 |
found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf)); |
488 |
state.local_filename = lyricStr; |
489 |
|
490 |
if (! found_lyricfile)
|
491 |
{ |
492 |
/* JWT:LOCAL LYRIC FILE NOT FOUND, SO CHECK THE GLOBAL CONFIG PATH FOR A MATCHING LYRICS FILE: */
|
493 |
lyricStr = String (str_concat ({aud_get_path (AudPath::UserDir), "/lyrics/",
|
494 |
str_decode_percent (base, ln), ".lrc"}));
|
495 |
found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf)); |
496 |
} |
497 |
} |
498 |
} |
499 |
|
500 |
Tuple tuple = aud_drct_get_tuple (); |
501 |
state.title = tuple.get_str (Tuple::Title); |
502 |
state.artist = tuple.get_str (Tuple::Artist); |
503 |
state.ok2save = false;
|
504 |
|
505 |
if (found_lyricfile) // JWT:WE HAVE LYRICS STORED IN A LOCAL FILE MATCHING FILE NAME!: |
506 |
{ |
507 |
AUDINFO ("i:Local lyric file found (%s).\n", (const char *) lyricStr); |
508 |
vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
|
509 |
} |
510 |
else // NO LOCAL LYRICS FILE FOUND, SO CHECK FOR LYRIC FILE MATCHING TITLE: |
511 |
{ |
512 |
if (state.title)
|
513 |
{ |
514 |
/* JWT:MANY STREAMS & SOME FILES FORMAT THE TITLE FIELD AS:
|
515 |
"[Artist: ]<artist> - [Title: ]<title> [<other-stuff>?]". IF SO, THEN PARSE OUT THE
|
516 |
ARTIST AND TITLE COMPONENTS FROM THE TITLE FOR SEARCHING LYRICWIKI:
|
517 |
*/
|
518 |
const char * ttlstart = (const char *) state.title; |
519 |
int ttllen = strlen (ttlstart); // MAKE SURE WE DON'T OVERRUN (THIS IS C)! ;^) |
520 |
if (ttllen > 8 && ! strcmp_nocase (ttlstart, "Artist:", 7)) |
521 |
ttlstart += 8;
|
522 |
|
523 |
if (ttllen > 0) // MAKE SURE WE DON'T OVERRUN! |
524 |
{ |
525 |
const char * ttloffset = ttlstart ? strstr (ttlstart, " - ") : nullptr; |
526 |
if (ttloffset)
|
527 |
{ |
528 |
state.artist = String (str_copy (ttlstart, (ttloffset-ttlstart))); |
529 |
ttloffset += 3;
|
530 |
ttllen -= 3;
|
531 |
if (ttllen > 7 && ! strcmp_nocase (ttloffset, "Title:", 6)) |
532 |
ttloffset += 7;
|
533 |
|
534 |
ttllen = strlen (ttloffset); |
535 |
if (ttllen > 0) |
536 |
{ |
537 |
const char * ttlend = strstr (ttloffset, " - "); |
538 |
if (ttlend)
|
539 |
state.title = String (str_copy (ttloffset, ttlend-ttloffset)); |
540 |
else
|
541 |
{ |
542 |
auto split = str_list_to_index (ttloffset, "|/"); |
543 |
for (auto & str : split) |
544 |
{ |
545 |
int ttllen_1 = strlen (str) - 1; // "CHOMP" ANY TRAILING SPACES: |
546 |
while (ttllen_1 >= 0 && str[ttllen_1] == ' ') |
547 |
ttllen_1--; |
548 |
|
549 |
if (ttllen_1 >= 0) |
550 |
{ |
551 |
StringBuf titleBuf = str_copy (str); |
552 |
titleBuf.resize (ttllen_1+1);
|
553 |
state.title = String (titleBuf); |
554 |
} |
555 |
break;
|
556 |
} |
557 |
} |
558 |
} |
559 |
} |
560 |
if (! state.local_filename || ! state.local_filename[0]) |
561 |
{ |
562 |
/* JWT:NO LOCAL LYRIC FILE, SO TRY SEARCH FOR LYRIC FILE BY TITLE: */
|
563 |
StringBuf titleBuf = str_copy (state.title); |
564 |
/* DON'T %-ENCODE SPACES BY CONVERTING TO A "LEGAL" CHAR. NOT (LIKELY) IN FILE-NAMES/TITLES: */
|
565 |
str_replace_char (titleBuf, ' ', '~'); |
566 |
titleBuf = str_encode_percent ((const char *) titleBuf, -1); |
567 |
str_replace_char (titleBuf, '~', ' '); // (THEN CONVERT 'EM BACK TO SPACES) |
568 |
if (path)
|
569 |
{ |
570 |
/* ENTRY IS A LOCAL FILE, SO FIRST CHECK DIRECTORY THE FILE IS IN: */
|
571 |
lyricStr = String (str_concat ({path, "/", titleBuf, ".lrc"})); |
572 |
found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf)); |
573 |
state.local_filename = lyricStr; |
574 |
if (found_lyricfile)
|
575 |
{ |
576 |
AUDINFO ("i:Local lyric file found by title (%s).\n", (const char *) lyricStr); |
577 |
vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
|
578 |
return;
|
579 |
} |
580 |
} |
581 |
/* OTHERWISE (STREAM, ETC.), CHECK THE GLOBAL CONFIG PATH FOR LYRICS FILE MATCHING TITLE: */
|
582 |
lyricStr = String (str_concat ({aud_get_path (AudPath::UserDir), |
583 |
"/lyrics/", titleBuf, ".lrc"})); |
584 |
found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf)); |
585 |
state.local_filename = lyricStr; |
586 |
if (found_lyricfile)
|
587 |
{ |
588 |
AUDINFO ("i:Global lyric file found by title (%s).\n", (const char *) lyricStr); |
589 |
vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
|
590 |
return;
|
591 |
} |
592 |
} |
593 |
} |
594 |
} |
595 |
/* IF HERE, NO LOCAL LYRICS FILE BY FILENAME OR TITLE, SEARCH LYRICWIKI: */
|
596 |
AUDINFO ("i:No Local lyric file found, try fetching from lyricwiki...\n");
|
597 |
get_lyrics_step_1 (); |
598 |
} |
599 |
lyricStr = String (); |
600 |
} |
601 |
|
602 |
static void lw_cleanup (QObject * object = nullptr) |
603 |
{ |
604 |
state.filename = String (); |
605 |
state.title = String (); |
606 |
state.artist = String (); |
607 |
state.uri = String (); |
608 |
state.local_filename = String (); |
609 |
|
610 |
hook_dissociate ("tuple change", (HookFunction) lyricwiki_playback_began);
|
611 |
hook_dissociate ("playback ready", (HookFunction) lyricwiki_playback_began);
|
612 |
|
613 |
textedit = nullptr;
|
614 |
} |
615 |
|
616 |
void * LyricWikiQt::get_qt_widget ()
|
617 |
{ |
618 |
textedit = new TextEdit;
|
619 |
textedit->setReadOnly (true);
|
620 |
|
621 |
#ifdef Q_OS_MAC // Mac-specific font tweaks |
622 |
textedit->document ()->setDefaultFont (QApplication::font ("QTipLabel"));
|
623 |
#endif
|
624 |
|
625 |
hook_associate ("tuple change", (HookFunction) lyricwiki_playback_began, nullptr); |
626 |
hook_associate ("playback ready", (HookFunction) lyricwiki_playback_began, nullptr); |
627 |
|
628 |
if (aud_drct_get_ready ())
|
629 |
lyricwiki_playback_began (); |
630 |
|
631 |
QObject::connect (textedit, & QObject::destroyed, lw_cleanup); |
632 |
|
633 |
return textedit;
|
634 |
} |
635 |
|
636 |
void TextEdit::contextMenuEvent (QContextMenuEvent * event)
|
637 |
{ |
638 |
QMenu * menu = createStandardContextMenu (); |
639 |
menu->addSeparator (); |
640 |
|
641 |
if (state.uri)
|
642 |
{ |
643 |
QAction * edit = menu->addAction (_("Edit Lyricwiki"));
|
644 |
QObject::connect (edit, & QAction::triggered, [] () { |
645 |
QDesktopServices::openUrl (QUrl ((const char *) state.uri)); |
646 |
}); |
647 |
} |
648 |
if (state.ok2save)
|
649 |
{ |
650 |
QAction * save_button = menu->addAction (_("Save Locally"));
|
651 |
QObject::connect (save_button, & QAction::triggered, [] () { |
652 |
save_lyrics_locally (); |
653 |
}); |
654 |
} |
655 |
menu->exec (event->globalPos ()); |
656 |
delete menu;
|
657 |
} |