summaryrefslogtreecommitdiff
path: root/Userland/Applications/SoundPlayer/Playlist.cpp
blob: 69db139f07b4ef4198d28502d02ff45bb5c43b3d (plain)
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
/*
 * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
 * Copyright (c) 2021, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include "Playlist.h"

#include <AK/LexicalPath.h>
#include <AK/Random.h>
#include <LibAudio/Loader.h>
#include <LibGUI/MessageBox.h>

bool Playlist::load(StringView path)
{
    auto parser = M3UParser::from_file(path);
    auto items = parser->parse(true);

    if (items->size() <= 0)
        return false;

    try_fill_missing_info(*items, path);
    for (auto& item : *items)
        m_model->items().append(item);
    m_model->invalidate();

    return true;
}

void Playlist::try_fill_missing_info(Vector<M3UEntry>& entries, StringView path)
{
    LexicalPath playlist_path(path);
    Vector<M3UEntry*> to_delete;

    for (auto& entry : entries) {
        if (!LexicalPath { entry.path }.is_absolute())
            entry.path = String::formatted("{}/{}", playlist_path.dirname(), entry.path);

        if (!entry.extended_info->file_size_in_bytes.has_value()) {
            auto size = Core::File::size(entry.path);
            if (size.is_error())
                continue;
            entry.extended_info->file_size_in_bytes = size.value();
        } else if (!Core::File::exists(entry.path)) {
            to_delete.append(&entry);
            continue;
        }

        if (!entry.extended_info->track_display_title.has_value())
            entry.extended_info->track_display_title = LexicalPath::title(entry.path);

        if (!entry.extended_info->track_length_in_seconds.has_value()) {
            // TODO: Implement embedded metadata extractor for other audio formats
            if (auto reader = Audio::Loader::create(entry.path); !reader.is_error())
                entry.extended_info->track_length_in_seconds = reader.value()->total_samples() / reader.value()->sample_rate();
        }

        // TODO: Implement a metadata parser for the uncomfortably numerous popular embedded metadata formats
    }

    for (auto& entry : to_delete)
        entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
}

StringView Playlist::next()
{
    if (m_next_index_to_play >= size()) {
        if (!looping())
            return {};
        m_next_index_to_play = 0;
    }

    auto next = m_model->items().at(m_next_index_to_play).path;
    if (!shuffling()) {
        m_next_index_to_play++;
        return next;
    }

    // Try a few times getting an item to play that has not been
    // recently played.  But do not try too hard, as we don't want
    // to wait forever.
    int shuffle_try;
    int const max_times_to_try = min(4, size());
    for (shuffle_try = 0; shuffle_try < max_times_to_try; shuffle_try++) {
        if (!m_previously_played_paths.maybe_contains(next))
            break;

        m_next_index_to_play = get_random_uniform(size());
        next = m_model->items().at(m_next_index_to_play).path;
    }
    if (shuffle_try == max_times_to_try) {
        // If we tried too much, maybe it's time to try resetting
        // the bloom filter and start over.
        m_previously_played_paths.reset();
    }

    m_previously_played_paths.add(next);
    return next;
}

StringView Playlist::previous()
{
    m_next_index_to_play--;
    if (m_next_index_to_play < 0) {
        m_next_index_to_play = 0;
        return {};
    }
    return m_model->items().at(m_next_index_to_play).path;
}