Project

General

Profile

lyricwiki.cc

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

 
1
/*
2
 * Copyright (c) 2010 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
#include <gtk/gtk.h>
25
#include <libxml/parser.h>
26
#include <libxml/tree.h>
27
#include <libxml/HTMLparser.h>
28
#include <libxml/xpath.h>
29

    
30
#define AUD_GLIB_INTEGRATION
31
#include <libaudcore/drct.h>
32
#include <libaudcore/i18n.h>
33
#include <libaudcore/plugin.h>
34
#include <libaudcore/plugins.h>
35
#include <libaudcore/runtime.h>
36
#include <libaudcore/audstrings.h>
37
#include <libaudcore/hook.h>
38
#include <libaudcore/vfs_async.h>
39

    
40
class LyricWiki : public GeneralPlugin
41
{
42
public:
43
    static constexpr PluginInfo info = {
44
        N_("LyricWiki Plugin"),
45
        PACKAGE,
46
        nullptr, // about
47
        nullptr, // prefs
48
        PluginGLibOnly
49
    };
50

    
51
    constexpr LyricWiki () : GeneralPlugin (info, false) {}
52

    
53
    void * get_gtk_widget ();
54
};
55

    
56
EXPORT LyricWiki aud_plugin_instance;
57

    
58
typedef struct {
59
    String filename; /* of song file */
60
    String title, artist;
61
    String uri; /* URI we are trying to retrieve */
62
    String local_filename; /* JWT:CALCULATED LOCAL FILENAME TO SAVE LYRICS TO */
63
    gint startlyrics;      /* JWT:OFFSET IN LYRICS WINDOW WHERE LYRIC TEXT ACTUALLY STARTS */
64
} LyricsState;
65

    
66
static LyricsState state;
67

    
68
/*
69
 * Suppress libxml warnings, because lyricwiki does not generate anything near
70
 * valid HTML.
71
 */
72
static void libxml_error_handler (void * ctx, const char * msg, ...)
73
{
74
}
75

    
76
static CharPtr scrape_lyrics_from_lyricwiki_edit_page (const char * buf, int64_t len)
77
{
78
    xmlDocPtr doc;
79
    CharPtr ret;
80

    
81
    /*
82
     * temporarily set our error-handling functor to our suppression function,
83
     * but we have to set it back because other components of Audacious depend
84
     * on libxml and we don't want to step on their code paths.
85
     *
86
     * unfortunately, libxml is anti-social and provides us with no way to get
87
     * the previous error functor, so we just have to set it back to default after
88
     * parsing and hope for the best.
89
     */
90
    xmlSetGenericErrorFunc (nullptr, libxml_error_handler);
91
    doc = htmlReadMemory (buf, (int) len, nullptr, "utf-8", (HTML_PARSE_RECOVER | HTML_PARSE_NONET));
92
    xmlSetGenericErrorFunc (nullptr, nullptr);
93

    
94
    if (doc)
95
    {
96
        xmlXPathContextPtr xpath_ctx = nullptr;
97
        xmlXPathObjectPtr xpath_obj = nullptr;
98
        xmlNodePtr node = nullptr;
99

    
100
        xpath_ctx = xmlXPathNewContext (doc);
101
        if (! xpath_ctx)
102
            goto give_up;
103

    
104
        xpath_obj = xmlXPathEvalExpression ((xmlChar *) "//*[@id=\"wpTextbox1\"]", xpath_ctx);
105
        if (! xpath_obj)
106
            goto give_up;
107

    
108
        if (! xpath_obj->nodesetval->nodeMax)
109
            goto give_up;
110

    
111
        node = xpath_obj->nodesetval->nodeTab[0];
112
give_up:
113
        if (xpath_obj)
114
            xmlXPathFreeObject (xpath_obj);
115

    
116
        if (xpath_ctx)
117
            xmlXPathFreeContext (xpath_ctx);
118

    
119
        if (node)
120
        {
121
            xmlChar * lyric = xmlNodeGetContent (node);
122

    
123
            if (lyric)
124
            {
125
                GMatchInfo * match_info;
126
                GRegex * reg;
127

    
128
                reg = g_regex_new
129
                 ("<(lyrics?)>[[:space:]]*(.*?)[[:space:]]*</\\1>",
130
                 (GRegexCompileFlags) (G_REGEX_MULTILINE | G_REGEX_DOTALL),
131
                 (GRegexMatchFlags) 0, nullptr);
132
                g_regex_match (reg, (char *) lyric, G_REGEX_MATCH_NEWLINE_ANY, & match_info);
133

    
134
                ret.capture (g_match_info_fetch (match_info, 2));
135
                if (! strcmp_nocase (ret, "<!-- PUT LYRICS HERE (and delete this entire line) -->"))
136
                    ret.capture (g_strdup (_("No lyrics available")));
137

    
138
                g_regex_unref (reg);
139
            }
140

    
141
            xmlFree (lyric);
142
        }
143

    
144
        xmlFreeDoc (doc);
145
    }
146

    
147
    return ret;
148
}
149

    
150
static String scrape_uri_from_lyricwiki_search_result (const char * buf, int64_t len)
151
{
152
    xmlDocPtr doc;
153
    String uri;
154

    
155
    /*
156
     * workaround buggy lyricwiki search output where it cuts the lyrics
157
     * halfway through the UTF-8 symbol resulting in invalid XML.
158
     */
159
    GRegex * reg;
160

    
161
    reg = g_regex_new ("<(lyrics?)>.*</\\1>", (GRegexCompileFlags)
162
     (G_REGEX_MULTILINE | G_REGEX_DOTALL | G_REGEX_UNGREEDY),
163
     (GRegexMatchFlags) 0, nullptr);
164
    CharPtr newbuf (g_regex_replace_literal (reg, buf, len, 0, "", G_REGEX_MATCH_NEWLINE_ANY, nullptr));
165
    g_regex_unref (reg);
166

    
167
    /*
168
     * temporarily set our error-handling functor to our suppression function,
169
     * but we have to set it back because other components of Audacious depend
170
     * on libxml and we don't want to step on their code paths.
171
     *
172
     * unfortunately, libxml is anti-social and provides us with no way to get
173
     * the previous error functor, so we just have to set it back to default after
174
     * parsing and hope for the best.
175
     */
176
    xmlSetGenericErrorFunc (nullptr, libxml_error_handler);
177
    doc = xmlParseMemory (newbuf, strlen (newbuf));
178
    xmlSetGenericErrorFunc (nullptr, nullptr);
179

    
180
    if (doc != nullptr)
181
    {
182
        xmlNodePtr root, cur;
183

    
184
        root = xmlDocGetRootElement(doc);
185

    
186
        for (cur = root->xmlChildrenNode; cur; cur = cur->next)
187
        {
188
            if (xmlStrEqual(cur->name, (xmlChar *) "url"))
189
            {
190
                auto lyric = (char *) xmlNodeGetContent (cur);
191

    
192
                // If the lyrics are unknown, LyricWiki returns a broken link
193
                // to the edit page.  Extract the song ID (artist:title) from
194
                // the URI and recreate a working link.
195
                char * title = strstr (lyric, "title=");
196
                if (title)
197
                {
198
                    title += 6;
199

    
200
                    // Strip trailing "&action=edit"
201
                    char * amp = strchr (title, '&');
202
                    if (amp)
203
                        * amp = 0;
204

    
205
                    // Spaces get replaced with plus signs for some reason.
206
                    str_replace_char (title, '+', ' ');
207

    
208
                    // LyricWiki interprets UTF-8 as ISO-8859-1, then "converts"
209
                    // it to UTF-8 again.  Worse, it will sometimes corrupt only
210
                    // the song title in this way while leaving the artist name
211
                    // intact.  So we have to percent-decode the URI, split the
212
                    // two strings apart, repair them separately, and then
213
                    // rebuild the URI.
214
                    auto strings = str_list_to_index (str_decode_percent (title), ":");
215
                    for (String & s : strings)
216
                    {
217
                        StringBuf orig_utf8 = str_convert (s, -1, "UTF-8", "ISO-8859-1");
218
                        if (orig_utf8 && g_utf8_validate (orig_utf8, -1, nullptr))
219
                            s = String (orig_utf8);
220
                    }
221

    
222
                    uri = String (str_printf ("https://lyrics.fandom.com/index.php?"
223
                     "action=edit&title=%s", (const char *) str_encode_percent
224
                     (index_to_str_list (strings, ":"))));
225
                }
226
                else
227
                {
228
                    // Convert normal lyrics link to edit page link
229
                    char * slash = strrchr (lyric, '/');
230
                    if (slash)
231
                        uri = String (str_printf ("https://lyrics.fandom.com/index.php?"
232
                         "action=edit&title=%s", slash + 1));
233
                }
234

    
235
                xmlFree ((xmlChar *) lyric);
236
            }
237
        }
238

    
239
        xmlFreeDoc (doc);
240
    }
241

    
242
    return uri;
243
}
244

    
245
static void update_lyrics_window (const char * title, const char * artist,
246
 const char * lyrics, bool edit_enabled);
247

    
248
static GtkWidget * edit_button;
249
static GtkWidget * save_button;
250

    
251
static void get_lyrics_step_3 (const char * uri, const Index<char> & buf, void *)
252
{
253
    if (! state.uri || strcmp (state.uri, uri))
254
        return;
255

    
256
    if (! buf.len ())
257
    {
258
        update_lyrics_window (_("Error"), nullptr,
259
         str_printf (_("Unable to fetch %s"), uri), true);
260
        return;
261
    }
262

    
263
    CharPtr lyrics = scrape_lyrics_from_lyricwiki_edit_page (buf.begin (), buf.len ());
264

    
265
    if (! lyrics)
266
    {
267
        update_lyrics_window (_("No lyrics Found"),
268
                (const char *) str_concat ({"Title: ", (const char *) state.title, "\nArtist: ",
269
                        (const char *) state.artist}),
270
                str_printf (_("Unable to parse %s"), uri), true);
271
        return;
272
    }
273

    
274
    update_lyrics_window (state.title, state.artist, lyrics, true);
275
    gtk_widget_set_sensitive (save_button, true);
276
}
277

    
278
static void get_lyrics_step_2 (const char * uri1, const Index<char> & buf, void *)
279
{
280
    if (! state.uri || strcmp (state.uri, uri1))
281
        return;
282

    
283
    if (! buf.len ())
284
    {
285
        update_lyrics_window (_("Error"), nullptr,
286
         str_printf (_("Unable to fetch %s"), uri1), false);
287
        return;
288
    }
289

    
290
    String uri = scrape_uri_from_lyricwiki_search_result (buf.begin (), buf.len ());
291

    
292
    if (! uri)
293
    {
294
        update_lyrics_window (_("Error"), nullptr,
295
         str_printf (_("Unable to parse %s"), uri1), false);
296
        return;
297
    }
298

    
299
    state.uri = uri;
300

    
301
    update_lyrics_window (state.title, state.artist, _("Looking for lyrics ..."), true);
302
    vfs_async_file_get_contents (uri, get_lyrics_step_3, nullptr);
303
}
304

    
305
static void get_lyrics_step_1 ()
306
{
307
    if (! state.artist || ! state.title)
308
    {
309
        update_lyrics_window (_("Error"), nullptr, _("Missing title and/or artist"), false);
310
        return;
311
    }
312

    
313
    StringBuf title_buf = str_encode_percent (state.title);
314
    StringBuf artist_buf = str_encode_percent (state.artist);
315

    
316
    state.uri = String (str_printf ("https://lyrics.fandom.com/api.php?"
317
     "action=lyrics&artist=%s&song=%s&fmt=xml", (const char *) artist_buf,
318
     (const char *) title_buf));
319

    
320
    update_lyrics_window (state.title, state.artist, _("Connecting to lyrics.fandom.com ..."), false);
321
    vfs_async_file_get_contents (state.uri, get_lyrics_step_2, nullptr);
322
}
323

    
324
static void get_lyrics_step_0 (const char * uri, const Index<char> & buf, void *)
325
{
326
    if (! buf.len ())
327
    {
328
        update_lyrics_window (_("Error"), nullptr,
329
         str_printf (_("Unable to fetch file %s"), uri), true);
330
        return;
331
    }
332

    
333
    StringBuf nullterminated_buf = str_copy (buf.begin (), buf.len ());
334
    update_lyrics_window (state.title, state.artist, (const char *) nullterminated_buf, false);
335

    
336
    /* JWT:ALLOW 'EM TO EDIT LYRICWIKI, EVEN IF LYRICS ARE LOCAL, IF THEY HAVE BOTH REQUIRED FIELDS: */
337
    if (state.artist && state.title)
338
    {
339
        StringBuf title_buf = str_copy (state.title);
340
        str_replace_char (title_buf, ' ', '_');
341
        title_buf = str_encode_percent (title_buf, -1);
342
        StringBuf artist_buf = str_copy (state.artist);
343
        str_replace_char (artist_buf, ' ', '_');
344
        artist_buf = str_encode_percent (artist_buf, -1);
345
        state.uri = String (str_printf ("https://lyrics.fandom.com/index.php?action=edit&title=%s:%s",
346
                (const char *) artist_buf, (const char *) title_buf));
347
        gtk_widget_set_sensitive (edit_button, true);
348
    }
349
}
350

    
351
static GtkTextView * textview;
352
static GtkTextBuffer * textbuffer;
353

    
354
static void launch_edit_page ()
355
{
356
    if (state.uri)
357
        gtk_show_uri (nullptr, state.uri, GDK_CURRENT_TIME, nullptr);
358
}
359

    
360
static void save_lyrics_locally ()
361
{
362
    if (state.local_filename && textbuffer)
363
    {
364
        GtkTextIter start_iter;
365
        GtkTextIter end_iter;
366
        gtk_text_buffer_get_start_iter (textbuffer, & start_iter);
367
        gtk_text_buffer_get_end_iter (textbuffer, & end_iter);
368
        gint sz = gtk_text_iter_get_offset (& end_iter) - gtk_text_iter_get_offset (& start_iter);
369
        sz -= state.startlyrics;
370
        if (sz > 0)
371
        {
372
            gchar * lyrics = gtk_text_buffer_get_slice (textbuffer, & start_iter,
373
                  & end_iter, false);
374
            if (lyrics)
375
            {
376
                if (strstr ((const char *) state.local_filename, "/lyrics/"))
377
                {
378
                    GStatBuf statbuf;
379
                    StringBuf path = filename_get_parent ((const char *) state.local_filename);
380
                    if (g_stat ((const char *) path, & statbuf)
381
                            && g_mkdir ((const char *) path, 0755))
382
                    {
383
                        AUDERR ("e:Could not create missing lyrics directory (%s)!\n", (const char *) path);
384
                        return;
385
                    }
386
                }
387
                VFSFile file (state.local_filename, "w");
388
                if (file)
389
                {
390
                    if (file.fwrite (lyrics + state.startlyrics, 1, sz) == sz)
391
                        AUDINFO ("i:Successfully saved %d bytes of lyrics locally to (%s).\n", sz,
392
                                (const char *) state.local_filename);
393

    
394
                    gtk_widget_set_sensitive (save_button, false);
395
                }
396
                g_free (lyrics);
397
            }
398
        }
399
    }
400
}
401

    
402
static GtkWidget * build_widget ()
403
{
404
    textview = (GtkTextView *) gtk_text_view_new ();
405
    gtk_text_view_set_editable (textview, false);
406
    gtk_text_view_set_cursor_visible (textview, false);
407
    gtk_text_view_set_left_margin (textview, 4);
408
    gtk_text_view_set_right_margin (textview, 4);
409
    gtk_text_view_set_wrap_mode (textview, GTK_WRAP_WORD);
410
    textbuffer = gtk_text_view_get_buffer (textview);
411

    
412
    GtkWidget * scrollview = gtk_scrolled_window_new (nullptr, nullptr);
413
    gtk_scrolled_window_set_shadow_type ((GtkScrolledWindow *) scrollview, GTK_SHADOW_IN);
414
    gtk_scrolled_window_set_policy ((GtkScrolledWindow *) scrollview, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
415
    GtkWidget * vbox = gtk_vbox_new (false, 6);
416

    
417
    gtk_container_add ((GtkContainer *) scrollview, (GtkWidget *) textview);
418
    gtk_box_pack_start ((GtkBox *) vbox, scrollview, true, true, 0);
419

    
420
    gtk_widget_show_all (vbox);
421

    
422
    gtk_text_buffer_create_tag (textbuffer, "weight_bold", "weight", PANGO_WEIGHT_BOLD, nullptr);
423
    gtk_text_buffer_create_tag (textbuffer, "size_x_large", "scale", PANGO_SCALE_X_LARGE, nullptr);
424
    gtk_text_buffer_create_tag (textbuffer, "style_italic", "style", PANGO_STYLE_ITALIC, nullptr);
425

    
426
    GtkWidget * hbox = gtk_hbox_new (false, 6);
427
    gtk_box_pack_start ((GtkBox *) vbox, hbox, false, false, 0);
428

    
429
    edit_button = gtk_button_new_with_mnemonic (_("Edit Lyricwiki"));
430
    gtk_widget_set_sensitive (edit_button, false);
431
    gtk_box_pack_end ((GtkBox *) hbox, edit_button, false, false, 0);
432

    
433
    save_button = gtk_button_new_with_mnemonic (_("Save Locally"));
434
    gtk_widget_set_sensitive (save_button, false);
435
    gtk_box_pack_end ((GtkBox *) hbox, save_button, false, false, 0);
436

    
437
    g_signal_connect (edit_button, "clicked", (GCallback) launch_edit_page, nullptr);
438
    g_signal_connect (save_button, "clicked", (GCallback) save_lyrics_locally, nullptr);
439

    
440
    return vbox;
441
}
442

    
443
static void update_lyrics_window (const char * title, const char * artist,
444
 const char * lyrics, bool edit_enabled)
445
{
446
    GtkTextIter iter, startlyrics;
447

    
448
    if (! textbuffer)
449
        return;
450

    
451
    gtk_text_buffer_set_text (textbuffer, "", -1);
452

    
453
    gtk_text_buffer_get_start_iter (textbuffer, & iter);
454

    
455
    gtk_text_buffer_insert_with_tags_by_name (textbuffer, & iter, title, -1,
456
     "weight_bold", "size_x_large", nullptr);
457

    
458
    if (artist)
459
    {
460
        gtk_text_buffer_insert (textbuffer, & iter, "\n", -1);
461
        gtk_text_buffer_insert_with_tags_by_name (textbuffer, & iter, artist, -1,
462
         "style_italic", nullptr);
463
    }
464

    
465
    gtk_text_buffer_insert (textbuffer, & iter, "\n\n", -1);
466
    gtk_text_buffer_get_end_iter (textbuffer, & startlyrics);
467
    state.startlyrics = gtk_text_iter_get_offset (& startlyrics);
468
    gtk_text_buffer_insert (textbuffer, & iter, lyrics, -1);
469

    
470
    gtk_text_buffer_get_start_iter (textbuffer, & iter);
471
    gtk_text_view_scroll_to_iter (textview, & iter, 0, true, 0, 0);
472

    
473
    gtk_widget_set_sensitive (edit_button, edit_enabled);
474
}
475

    
476
static void lyricwiki_playback_began ()
477
{
478
    /* FIXME: cancel previous VFS requests (not possible with current API) */
479

    
480
    bool found_lyricfile = false;
481
    GStatBuf statbuf;
482
    String lyricStr = String ("");
483
    StringBuf path = StringBuf ();
484

    
485
    state.filename = aud_drct_get_filename ();
486
    state.uri = String ();
487

    
488
    state.local_filename = String ("");
489
    if (! strncmp ((const char *) state.filename, "file://", 7))  // JWT:WE'RE A LOCAL FILE, CHECK FOR CORRESPONDING LYRICS FILE:
490
    {
491
        /* JWT: EXTRACT JUST THE "NAME" PART TO USE TO NAME THE LYRICS FILE: */
492
        const char * slash = state.filename ? strrchr (state.filename, '/') : nullptr;
493
        const char * base = slash ? slash + 1 : nullptr;
494

    
495
        if (base && base[0])
496
        {
497
            /* JWT:FIRST CHECK LOCAL DIRECTORY FOR A LYRICS FILE MATCHING FILE-NAME: */
498
            const char * dot = strrchr (base, '.');
499
            int ln = (dot && ! strstr (dot, ".cue?")) ? (dot - base) : -1;  // SET TO FULL LENGTH(-1) IF NO EXTENSION OR NOT A CUESHEET.
500
            path = filename_get_parent (uri_to_filename (state.filename));
501
            lyricStr = String (str_concat ({path, "/", str_decode_percent (base, ln), ".lrc"}));
502
            found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf));
503
            state.local_filename = lyricStr;
504

    
505
            if (! found_lyricfile)
506
            {
507
                /* JWT:LOCAL LYRIC FILE NOT FOUND, SO CHECK THE GLOBAL CONFIG PATH FOR A MATCHING LYRICS FILE: */
508
                lyricStr = String (str_concat ({aud_get_path (AudPath::UserDir), "/lyrics/",
509
                        str_decode_percent (base, ln), ".lrc"}));
510
                found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf));
511
            }
512
        }
513
    }
514

    
515
    Tuple tuple = aud_drct_get_tuple ();
516
    state.title = tuple.get_str (Tuple::Title);
517
    state.artist = tuple.get_str (Tuple::Artist);
518
    gtk_widget_set_sensitive (save_button, false);
519

    
520
    if (found_lyricfile)  // JWT:WE HAVE LYRICS STORED IN A LOCAL FILE MATCHING FILE NAME!:
521
    {
522
        AUDINFO ("i:Local lyric file found (%s).\n", (const char *) lyricStr);
523
        vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
524
    }
525
    else  // NO LOCAL LYRICS FILE FOUND, SO CHECK FOR LYRIC FILE MATCHING TITLE:
526
    {
527
        if (state.title)
528
        {
529
            /* JWT:MANY STREAMS & SOME FILES FORMAT THE TITLE FIELD AS:
530
               "[Artist: ]<artist> - [Title: ]<title> [<other-stuff>?]".  IF SO, THEN PARSE OUT THE
531
               ARTIST AND TITLE COMPONENTS FROM THE TITLE FOR SEARCHING LYRICWIKI:
532
            */
533
            const char * ttlstart = (const char *) state.title;
534
            int ttllen = strlen (ttlstart);  // MAKE SURE WE DON'T OVERRUN (THIS IS C)! ;^)
535
            if (ttllen > 8 && ! strcmp_nocase (ttlstart, "Artist:", 7))
536
                ttlstart += 8;
537

    
538
            if (ttllen > 0)  // MAKE SURE WE DON'T OVERRUN!
539
            {
540
                const char * ttloffset = ttlstart ? strstr (ttlstart, " - ") : nullptr;
541
                if (ttloffset)
542
                {
543
                    state.artist = String (str_copy (ttlstart, (ttloffset-ttlstart)));
544
                    ttloffset += 3;
545
                    ttllen -= 3;
546
                    if (ttllen > 7 && ! strcmp_nocase (ttloffset, "Title:", 6))
547
                        ttloffset += 7;
548

    
549
                    ttllen = strlen (ttloffset);
550
                    if (ttllen > 0)
551
                    {
552
                        const char * ttlend = strstr (ttloffset, " - ");
553
                        if (ttlend)
554
                            state.title = String (str_copy (ttloffset, ttlend-ttloffset));
555
                        else
556
                        {
557
                            auto split = str_list_to_index (ttloffset, "|/");
558
                            for (auto & str : split)
559
                            {
560
                                int ttllen_1 = strlen (str) - 1;  // "CHOMP" ANY TRAILING SPACES:
561
                                while (ttllen_1 >= 0 && str[ttllen_1] == ' ')
562
                                    ttllen_1--;
563

    
564
                                if (ttllen_1 >= 0)
565
                                {
566
                                    StringBuf titleBuf = str_copy (str);
567
                                    titleBuf.resize (ttllen_1+1);
568
                                    state.title = String (titleBuf);
569
                                }
570
                                break;
571
                            }
572
                        }
573
                    }
574
                }
575
                if (! state.local_filename || ! state.local_filename[0])
576
                {
577
                    /* JWT:NO LOCAL LYRIC FILE, SO TRY SEARCH FOR LYRIC FILE BY TITLE: */
578
                    StringBuf titleBuf = str_copy (state.title);
579
                    /* DON'T %-ENCODE SPACES BY CONVERTING TO A "LEGAL" CHAR. NOT (LIKELY) IN FILE-NAMES/TITLES: */
580
                    str_replace_char (titleBuf, ' ', '~');
581
                    titleBuf = str_encode_percent ((const char *) titleBuf, -1);
582
                    str_replace_char (titleBuf, '~', ' ');  // (THEN CONVERT 'EM BACK TO SPACES)
583
                    if (path)
584
                    {
585
                        /* ENTRY IS A LOCAL FILE, SO FIRST CHECK DIRECTORY THE FILE IS IN: */
586
                        lyricStr = String (str_concat ({path, "/", titleBuf, ".lrc"}));
587
                        found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf));
588
                        state.local_filename = lyricStr;
589
                        if (found_lyricfile)
590
                        {
591
                            AUDINFO ("i:Local lyric file found by title (%s).\n", (const char *) lyricStr);
592
                            vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
593
                            return;
594
                        }
595
                    }
596
                    /* OTHERWISE (STREAM, ETC.), CHECK THE GLOBAL CONFIG PATH FOR LYRICS FILE MATCHING TITLE: */
597
                    lyricStr = String (str_concat ({aud_get_path (AudPath::UserDir),
598
                            "/lyrics/", titleBuf, ".lrc"}));
599
                    found_lyricfile = ! (g_stat ((const char *) lyricStr, & statbuf));
600
                    state.local_filename = lyricStr;
601
                    if (found_lyricfile)
602
                    {
603
                        AUDINFO ("i:Global lyric file found by title (%s).\n", (const char *) lyricStr);
604
                        vfs_async_file_get_contents (lyricStr, get_lyrics_step_0, nullptr);
605
                        return;
606
                    }
607
                }
608
            }
609
        }
610
        /* IF HERE, NO LOCAL LYRICS FILE BY FILENAME OR TITLE, SEARCH LYRICWIKI: */
611
        AUDINFO ("i:No Local lyric file found, try fetching from lyricwiki...\n");
612
        get_lyrics_step_1 ();
613
    }
614
    lyricStr = String ();
615
}
616

    
617
static void destroy_cb ()
618
{
619
    state.filename = String ();
620
    state.title = String ();
621
    state.artist = String ();
622
    state.uri = String ();
623
    state.local_filename = String ();
624

    
625
    hook_dissociate ("tuple change", (HookFunction) lyricwiki_playback_began);
626
    hook_dissociate ("playback ready", (HookFunction) lyricwiki_playback_began);
627

    
628
    textview = nullptr;
629
    textbuffer = nullptr;
630
    save_button = nullptr;
631
    edit_button = nullptr;
632
}
633

    
634
void * LyricWiki::get_gtk_widget ()
635
{
636
    GtkWidget * vbox = build_widget ();
637

    
638
    hook_associate ("tuple change", (HookFunction) lyricwiki_playback_began, nullptr);
639
    hook_associate ("playback ready", (HookFunction) lyricwiki_playback_began, nullptr);
640

    
641
    if (aud_drct_get_ready ())
642
        lyricwiki_playback_began ();
643

    
644
    g_signal_connect (vbox, "destroy", destroy_cb, nullptr);
645

    
646
    return vbox;
647
}