Project

General

Profile

lyricwiki-qt.cc

lyricwiki - patched (Qt) - Jim Turner, October 21, 2019 15:10

 
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
}