File: test_lyrics.py

package info (click to toggle)
beets 2.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 7,988 kB
  • sloc: python: 46,429; javascript: 8,018; xml: 334; sh: 261; makefile: 125
file content (681 lines) | stat: -rw-r--r-- 22,918 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# This file is part of beets.
# Copyright 2016, Fabrice Laporte.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Tests for the 'lyrics' plugin."""

import re
import textwrap
from functools import partial
from http import HTTPStatus
from pathlib import Path

import pytest

from beets.library import Item
from beets.test.helper import PluginMixin, TestHelper
from beetsplug import lyrics

from .lyrics_pages import LyricsPage, lyrics_pages

PHRASE_BY_TITLE = {
    "Lady Madonna": "friday night arrives without a suitcase",
    "Jazz'n'blues": "as i check my balance i kiss the screen",
    "Beets song": "via plugins, beets becomes a panacea",
}


@pytest.fixture(scope="module")
def helper():
    helper = TestHelper()
    helper.setup_beets()
    yield helper
    helper.teardown_beets()


class TestLyricsUtils:
    @pytest.mark.parametrize(
        "artist, title",
        [
            ("Various Artists", "Title"),
            ("Artist", ""),
            ("", "Title"),
            (" ", ""),
            ("", " "),
            ("", ""),
        ],
    )
    def test_search_empty(self, artist, title):
        actual_pairs = lyrics.search_pairs(Item(artist=artist, title=title))

        assert not list(actual_pairs)

    @pytest.mark.parametrize(
        "artist, artist_sort, expected_extra_artists",
        [
            ("Alice ft. Bob", "", ["Alice"]),
            ("Alice feat Bob", "", ["Alice"]),
            ("Alice feat. Bob", "", ["Alice"]),
            ("Alice feats Bob", "", []),
            ("Alice featuring Bob", "", ["Alice"]),
            ("Alice & Bob", "", ["Alice"]),
            ("Alice and Bob", "", ["Alice"]),
            ("Alice", "", []),
            ("Alice", "Alice", []),
            ("Alice", "alice", []),
            ("Alice", "alice ", []),
            ("Alice", "Alice A", ["Alice A"]),
            ("CHVRCHΞS", "CHVRCHES", ["CHVRCHES"]),
            ("横山克", "Masaru Yokoyama", ["Masaru Yokoyama"]),
        ],
    )
    def test_search_pairs_artists(
        self, artist, artist_sort, expected_extra_artists
    ):
        item = Item(artist=artist, artist_sort=artist_sort, title="song")

        actual_artists = [a for a, _ in lyrics.search_pairs(item)]

        # Make sure that the original artist name is still the first entry
        assert actual_artists == [artist, *expected_extra_artists]

    @pytest.mark.parametrize(
        "title, expected_extra_titles",
        [
            ("1/2", []),
            ("1 / 2", ["1", "2"]),
            ("Song (live)", ["Song"]),
            ("Song (live) (new)", ["Song"]),
            ("Song (live (new))", ["Song"]),
            ("Song ft. B", ["Song"]),
            ("Song featuring B", ["Song"]),
            ("Song and B", []),
            ("Song: B", ["Song"]),
        ],
    )
    def test_search_pairs_titles(self, title, expected_extra_titles):
        item = Item(title=title, artist="A")

        actual_titles = {
            t: None for _, tit in lyrics.search_pairs(item) for t in tit
        }

        assert list(actual_titles) == [title, *expected_extra_titles]

    @pytest.mark.parametrize(
        "text, expected",
        [
            ("test", "test"),
            ("Mørdag", "mordag"),
            ("l'été c'est fait pour jouer", "l-ete-c-est-fait-pour-jouer"),
            ("\xe7afe au lait (boisson)", "cafe-au-lait-boisson"),
            ("Multiple  spaces -- and symbols! -- merged", "multiple-spaces-and-symbols-merged"),  # noqa: E501
            ("\u200bno-width-space", "no-width-space"),
            ("El\u002dp", "el-p"),
            ("\u200bblackbear", "blackbear"),
            ("\u200d", ""),
            ("\u2010", ""),
        ],
    )  # fmt: skip
    def test_slug(self, text, expected):
        assert lyrics.slug(text) == expected


class TestHtml:
    def test_scrape_strip_cruft(self):
        initial = """<!--lyrics below-->
                  &nbsp;one
                  <br class='myclass'>
                  two  !
                  <br><br \\>
                  <blink>four</blink>"""
        expected = "<!--lyrics below-->\none\ntwo !\n\n<blink>four</blink>"

        assert lyrics.Html.normalize_space(initial) == expected

    def test_scrape_merge_paragraphs(self):
        text = "one</p>   <p class='myclass'>two</p><p>three"
        expected = "one\ntwo\n\nthree"

        assert lyrics.Html.merge_paragraphs(text) == expected


class TestSearchBackend:
    @pytest.fixture
    def backend(self, dist_thresh):
        plugin = lyrics.LyricsPlugin()
        plugin.config.set({"dist_thresh": dist_thresh})
        return lyrics.SearchBackend(plugin.config, plugin._log)

    @pytest.mark.parametrize(
        "dist_thresh, target_artist, artist, should_match",
        [
            (0.11, "Target Artist", "Target Artist", True),
            (0.11, "Target Artist", "Target Artis", True),
            (0.11, "Target Artist", "Target Arti", False),
            (0.11, "Psychonaut", "Psychonaut (BEL)", True),
            (0.11, "beets song", "beats song", True),
            (0.10, "beets song", "beats song", False),
            (
                0.11,
                "Lucid Dreams (Forget Me)",
                "Lucid Dreams (Remix) ft. Lil Uzi Vert",
                False,
            ),
            (
                0.12,
                "Lucid Dreams (Forget Me)",
                "Lucid Dreams (Remix) ft. Lil Uzi Vert",
                True,
            ),
        ],
    )
    def test_check_match(self, backend, target_artist, artist, should_match):
        result = lyrics.SearchResult(artist, "", "")

        assert backend.check_match(target_artist, "", result) == should_match


@pytest.fixture(scope="module")
def lyrics_root_dir(pytestconfig: pytest.Config):
    return pytestconfig.rootpath / "test" / "rsrc" / "lyrics"


class LyricsPluginMixin(PluginMixin):
    plugin = "lyrics"

    @pytest.fixture
    def plugin_config(self):
        """Return lyrics configuration to test."""
        return {}

    @pytest.fixture
    def lyrics_plugin(self, backend_name, plugin_config):
        """Set configuration and returns the plugin's instance."""
        plugin_config["sources"] = [backend_name]
        self.config[self.plugin].set(plugin_config)

        return lyrics.LyricsPlugin()


class TestLyricsPlugin(LyricsPluginMixin):
    @pytest.fixture
    def backend_name(self):
        """Return lyrics configuration to test."""
        return "lrclib"

    @pytest.mark.parametrize(
        "request_kwargs, expected_log_match",
        [
            (
                {"status_code": HTTPStatus.BAD_GATEWAY},
                r"LRCLib: Request error: 502",
            ),
            ({"text": "invalid"}, r"LRCLib: Could not decode.*JSON"),
        ],
    )
    def test_error_handling(
        self,
        requests_mock,
        lyrics_plugin,
        caplog,
        request_kwargs,
        expected_log_match,
    ):
        """Errors are logged with the backend name."""
        requests_mock.get(lyrics.LRCLib.SEARCH_URL, **request_kwargs)

        assert lyrics_plugin.get_lyrics("", "", "", 0.0) is None
        assert caplog.messages
        last_log = caplog.messages[-1]
        assert last_log
        assert re.search(expected_log_match, last_log, re.I)

    @pytest.mark.parametrize(
        "plugin_config, found, expected",
        [
            ({}, "new", "old"),
            ({"force": True}, "new", "new"),
            ({"force": True, "local": True}, "new", "old"),
            ({"force": True, "fallback": None}, "", "old"),
            ({"force": True, "fallback": ""}, "", ""),
            ({"force": True, "fallback": "default"}, "", "default"),
        ],
    )
    def test_overwrite_config(
        self, monkeypatch, helper, lyrics_plugin, found, expected
    ):
        monkeypatch.setattr(lyrics_plugin, "find_lyrics", lambda _: found)
        item = helper.create_item(id=1, lyrics="old")

        lyrics_plugin.add_item_lyrics(item, False)

        assert item.lyrics == expected


class LyricsBackendTest(LyricsPluginMixin):
    @pytest.fixture
    def backend(self, lyrics_plugin):
        """Return a lyrics backend instance."""
        return lyrics_plugin.backends[0]

    @pytest.fixture
    def lyrics_html(self, lyrics_root_dir, file_name):
        return (lyrics_root_dir / f"{file_name}.txt").read_text(
            encoding="utf-8"
        )


@pytest.mark.on_lyrics_update
class TestLyricsSources(LyricsBackendTest):
    @pytest.fixture(scope="class")
    def plugin_config(self):
        return {"google_API_key": "test", "synced": True}

    @pytest.fixture(
        params=[pytest.param(lp, marks=lp.marks) for lp in lyrics_pages],
        ids=str,
    )
    def lyrics_page(self, request):
        return request.param

    @pytest.fixture
    def backend_name(self, lyrics_page):
        return lyrics_page.backend

    @pytest.fixture(autouse=True)
    def _patch_google_search(self, requests_mock, lyrics_page):
        """Mock the Google Search API to return the lyrics page under test."""
        requests_mock.real_http = True

        data = {
            "items": [
                {
                    "title": lyrics_page.url_title,
                    "link": lyrics_page.url,
                    "displayLink": lyrics_page.root_url,
                }
            ]
        }
        requests_mock.get(lyrics.Google.SEARCH_URL, json=data)

    def test_backend_source(self, lyrics_plugin, lyrics_page: LyricsPage):
        """Test parsed lyrics from each of the configured lyrics pages."""
        lyrics_info = lyrics_plugin.find_lyrics(
            Item(
                artist=lyrics_page.artist,
                title=lyrics_page.track_title,
                album="",
                length=186.0,
            )
        )

        assert lyrics_info
        lyrics, _ = lyrics_info.split("\n\nSource: ")
        assert lyrics == lyrics_page.lyrics


class TestGoogleLyrics(LyricsBackendTest):
    """Test scraping heuristics on a fake html page."""

    @pytest.fixture(scope="class")
    def backend_name(self):
        return "google"

    @pytest.fixture
    def plugin_config(self):
        return {"google_API_key": "test"}

    @pytest.fixture(scope="class")
    def file_name(self):
        return "examplecom/beetssong"

    @pytest.fixture
    def search_item(self, url_title, url):
        return {"title": url_title, "link": url}

    @pytest.mark.parametrize("plugin_config", [{}])
    def test_disabled_without_api_key(self, lyrics_plugin):
        assert not lyrics_plugin.backends

    def test_mocked_source_ok(self, backend, lyrics_html):
        """Test that lyrics of the mocked page are correctly scraped"""
        result = backend.scrape(lyrics_html).lower()

        assert result
        assert PHRASE_BY_TITLE["Beets song"] in result

    @pytest.mark.parametrize(
        "url_title, expected_artist, expected_title",
        [
            ("Artist - beets song Lyrics", "Artist", "beets song"),
            ("www.azlyrics.com | Beats song by Artist", "Artist", "Beats song"),
            ("lyric.com | seets bong lyrics by Artist", "Artist", "seets bong"),
            ("foo", "", "foo"),
            ("Artist - Beets Song lyrics | AZLyrics", "Artist", "Beets Song"),
            ("Letra de Artist - Beets Song", "Artist", "Beets Song"),
            ("Letra de Artist - Beets ...", "Artist", "Beets"),
            ("Artist Beets Song", "Artist", "Beets Song"),
            ("BeetsSong - Artist", "Artist", "BeetsSong"),
            ("Artist - BeetsSong", "Artist", "BeetsSong"),
            ("Beets Song", "", "Beets Song"),
            ("Beets Song Artist", "Artist", "Beets Song"),
            (
                "BeetsSong (feat. Other & Another) - Artist",
                "Artist",
                "BeetsSong (feat. Other & Another)",
            ),
            (
                (
                    "Beets song lyrics by Artist - original song full text. "
                    "Official Beets song lyrics, 2024 version | LyricsMode.com"
                ),
                "Artist",
                "Beets song",
            ),
        ],
    )
    @pytest.mark.parametrize("url", ["http://doesntmatter.com"])
    def test_make_search_result(
        self, backend, search_item, expected_artist, expected_title
    ):
        result = backend.make_search_result("Artist", "Beets song", search_item)

        assert result.artist == expected_artist
        assert result.title == expected_title


class TestGeniusLyrics(LyricsBackendTest):
    @pytest.fixture(scope="class")
    def backend_name(self):
        return "genius"

    @pytest.mark.parametrize(
        "file_name, expected_line_count",
        [
            ("geniuscom/2pacalleyezonmelyrics", 131),
            ("geniuscom/Ttngchinchillalyrics", 29),
            ("geniuscom/sample", 0),  # see https://github.com/beetbox/beets/issues/3535
        ],
    )  # fmt: skip
    def test_scrape(self, backend, lyrics_html, expected_line_count):
        result = backend.scrape(lyrics_html) or ""

        assert len(result.splitlines()) == expected_line_count


class TestTekstowoLyrics(LyricsBackendTest):
    @pytest.fixture(scope="class")
    def backend_name(self):
        return "tekstowo"

    @pytest.mark.parametrize(
        "file_name, expecting_lyrics",
        [
            ("tekstowopl/piosenka24kgoldncityofangels1", True),
            (
                "tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement",  # noqa: E501
                False,
            ),
        ],
    )
    def test_scrape(self, backend, lyrics_html, expecting_lyrics):
        assert bool(backend.scrape(lyrics_html)) == expecting_lyrics


LYRICS_DURATION = 950


def lyrics_match(**overrides):
    return {
        "id": 1,
        "instrumental": False,
        "duration": LYRICS_DURATION,
        "syncedLyrics": "synced",
        "plainLyrics": "plain",
        **overrides,
    }


class TestLRCLibLyrics(LyricsBackendTest):
    ITEM_DURATION = 999

    @pytest.fixture(scope="class")
    def backend_name(self):
        return "lrclib"

    @pytest.fixture
    def fetch_lyrics(self, backend, requests_mock, response_data):
        requests_mock.get(backend.GET_URL, status_code=HTTPStatus.NOT_FOUND)
        requests_mock.get(backend.SEARCH_URL, json=response_data)

        return partial(backend.fetch, "la", "la", "la", self.ITEM_DURATION)

    @pytest.mark.parametrize("response_data", [[lyrics_match()]])
    @pytest.mark.parametrize(
        "plugin_config, expected_lyrics",
        [({"synced": True}, "synced"), ({"synced": False}, "plain")],
    )
    def test_synced_config_option(self, fetch_lyrics, expected_lyrics):
        lyrics, _ = fetch_lyrics()

        assert lyrics == expected_lyrics

    @pytest.mark.parametrize(
        "response_data, expected_lyrics",
        [
            pytest.param([], None, id="handle non-matching lyrics"),
            pytest.param(
                [lyrics_match()],
                "synced",
                id="synced when available",
            ),
            pytest.param(
                [lyrics_match(duration=1)],
                None,
                id="none: duration too short",
            ),
            pytest.param(
                [lyrics_match(instrumental=True)],
                "[Instrumental]",
                id="instrumental track",
            ),
            pytest.param(
                [lyrics_match(syncedLyrics=None)],
                "plain",
                id="plain by default",
            ),
            pytest.param(
                [
                    lyrics_match(
                        duration=ITEM_DURATION,
                        syncedLyrics=None,
                        plainLyrics="plain with closer duration",
                    ),
                    lyrics_match(syncedLyrics="synced", plainLyrics="plain 2"),
                ],
                "synced",
                id="prefer synced lyrics even if plain duration is closer",
            ),
            pytest.param(
                [
                    lyrics_match(
                        duration=ITEM_DURATION,
                        syncedLyrics=None,
                        plainLyrics="valid plain",
                    ),
                    lyrics_match(
                        duration=1,
                        syncedLyrics="synced with invalid duration",
                    ),
                ],
                "valid plain",
                id="ignore synced with invalid duration",
            ),
            pytest.param(
                [lyrics_match(syncedLyrics=None), lyrics_match()],
                "synced",
                id="prefer match with synced lyrics",
            ),
        ],
    )
    @pytest.mark.parametrize("plugin_config", [{"synced": True}])
    def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics):
        lyrics_info = fetch_lyrics()
        if lyrics_info is None:
            assert expected_lyrics is None
        else:
            lyrics, _ = fetch_lyrics()

            assert lyrics == expected_lyrics


class TestTranslation:
    @pytest.fixture(autouse=True)
    def _patch_bing(self, requests_mock):
        def callback(request, _):
            if b"Refrain" in request.body:
                translations = (
                    ""
                    " | [Refrain : Doja Cat]"
                    " | Difficile pour moi de te laisser partir (Te laisser partir, te laisser partir)"  # noqa: E501
                    " | Mon corps ne me laissait pas le cacher (Cachez-le)"
                    " | Quoi qu’il arrive, je ne plierais pas (Ne plierait pas, ne plierais pas)"  # noqa: E501
                    " | Chevauchant à travers le tonnerre, la foudre"
                )
            elif b"00:00.00" in request.body:
                translations = (
                    ""
                    " | [00:00.00] Quelques paroles synchronisées"
                    " | [00:01.00] Quelques paroles plus synchronisées"
                )
            else:
                translations = (
                    ""
                    " | Quelques paroles synchronisées"
                    " | Quelques paroles plus synchronisées"
                )

            return [
                {
                    "detectedLanguage": {"language": "en", "score": 1.0},
                    "translations": [{"text": translations, "to": "fr"}],
                }
            ]

        requests_mock.post(lyrics.Translator.TRANSLATE_URL, json=callback)

    @pytest.mark.parametrize(
        "new_lyrics, old_lyrics, expected",
        [
            pytest.param(
                """
                [Refrain: Doja Cat]
                Hard for me to let you go (Let you go, let you go)
                My body wouldn't let me hide it (Hide it)
                No matter what, I wouldn't fold (Wouldn't fold, wouldn't fold)
                Ridin' through the thunder, lightnin'""",
                "",
                """
                [Refrain: Doja Cat] / [Refrain : Doja Cat]
                Hard for me to let you go (Let you go, let you go) / Difficile pour moi de te laisser partir (Te laisser partir, te laisser partir)
                My body wouldn't let me hide it (Hide it) / Mon corps ne me laissait pas le cacher (Cachez-le)
                No matter what, I wouldn't fold (Wouldn't fold, wouldn't fold) / Quoi qu’il arrive, je ne plierais pas (Ne plierait pas, ne plierais pas)
                Ridin' through the thunder, lightnin' / Chevauchant à travers le tonnerre, la foudre""",  # noqa: E501
                id="plain",
            ),
            pytest.param(
                """
                [00:00.00] Some synced lyrics
                [00:00:50]
                [00:01.00] Some more synced lyrics

                Source: https://lrclib.net/api/123""",
                "",
                """
                [00:00.00] Some synced lyrics / Quelques paroles synchronisées
                [00:00:50]
                [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées

                Source: https://lrclib.net/api/123""",  # noqa: E501
                id="synced",
            ),
            pytest.param(
                "Quelques paroles",
                "",
                "Quelques paroles",
                id="already in the target language",
            ),
            pytest.param(
                "Some lyrics",
                "Some lyrics / Some translation",
                "Some lyrics / Some translation",
                id="already translated",
            ),
        ],
    )
    def test_translate(self, new_lyrics, old_lyrics, expected):
        plugin = lyrics.LyricsPlugin()
        bing = lyrics.Translator(plugin._log, "123", "FR", ["EN"])

        assert bing.translate(
            textwrap.dedent(new_lyrics), old_lyrics
        ) == textwrap.dedent(expected)


class TestRestFiles:
    @pytest.fixture
    def rest_dir(self, tmp_path):
        return tmp_path

    @pytest.fixture
    def rest_files(self, rest_dir):
        return lyrics.RestFiles(rest_dir)

    def test_write(self, rest_dir: Path, rest_files):
        items = [
            Item(albumartist=aa, album=a, title=t, lyrics=lyr)
            for aa, a, t, lyr in [
                ("Artist One", "Album One", "Song One", "Lyrics One"),
                ("Artist One", "Album One", "Song Two", "Lyrics Two"),
                ("Artist Two", "Album Two", "Song Three", "Lyrics Three"),
            ]
        ]

        rest_files.write(items)

        assert (rest_dir / "index.rst").exists()
        assert (rest_dir / "conf.py").exists()

        artist_one_file = rest_dir / "artists" / "artist-one.rst"
        artist_two_file = rest_dir / "artists" / "artist-two.rst"
        assert artist_one_file.exists()
        assert artist_two_file.exists()

        c = artist_one_file.read_text()
        assert (
            c.index("Artist One")
            < c.index("Album One")
            < c.index("Song One")
            < c.index("Lyrics One")
            < c.index("Song Two")
            < c.index("Lyrics Two")
        )

        c = artist_two_file.read_text()
        assert (
            c.index("Artist Two")
            < c.index("Album Two")
            < c.index("Song Three")
            < c.index("Lyrics Three")
        )