Serenity Operating System
1/*
2 * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
3 * Copyright (c) 2021, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "Playlist.h"
9
10#include <AK/LexicalPath.h>
11#include <AK/Random.h>
12#include <LibAudio/Loader.h>
13#include <LibCore/DeprecatedFile.h>
14#include <LibGUI/MessageBox.h>
15
16bool Playlist::load(StringView path)
17{
18 auto parser = M3UParser::from_file(path);
19 auto items = parser->parse(true);
20
21 if (items->size() <= 0)
22 return false;
23
24 try_fill_missing_info(*items, path);
25 for (auto& item : *items)
26 m_model->items().append(item);
27 m_model->invalidate();
28
29 return true;
30}
31
32void Playlist::try_fill_missing_info(Vector<M3UEntry>& entries, StringView path)
33{
34 LexicalPath playlist_path(path);
35 Vector<M3UEntry*> to_delete;
36
37 for (auto& entry : entries) {
38 if (!LexicalPath { entry.path }.is_absolute())
39 entry.path = DeprecatedString::formatted("{}/{}", playlist_path.dirname(), entry.path);
40
41 if (!entry.extended_info->file_size_in_bytes.has_value()) {
42 auto size = Core::DeprecatedFile::size(entry.path);
43 if (size.is_error())
44 continue;
45 entry.extended_info->file_size_in_bytes = size.value();
46 } else if (!Core::DeprecatedFile::exists(entry.path)) {
47 to_delete.append(&entry);
48 continue;
49 }
50
51 if (!entry.extended_info->track_display_title.has_value())
52 entry.extended_info->track_display_title = LexicalPath::title(entry.path);
53
54 if (!entry.extended_info->track_length_in_seconds.has_value()) {
55 // TODO: Implement embedded metadata extractor for other audio formats
56 if (auto reader = Audio::Loader::create(entry.path); !reader.is_error())
57 entry.extended_info->track_length_in_seconds = reader.value()->total_samples() / reader.value()->sample_rate();
58 }
59
60 // TODO: Implement a metadata parser for the uncomfortably numerous popular embedded metadata formats
61 }
62
63 for (auto& entry : to_delete)
64 entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
65}
66
67StringView Playlist::next()
68{
69 if (m_next_index_to_play >= size()) {
70 if (!looping())
71 return {};
72 m_next_index_to_play = 0;
73 }
74
75 auto next = m_model->items().at(m_next_index_to_play).path;
76 if (!shuffling()) {
77 m_next_index_to_play++;
78 return next;
79 }
80
81 // Try a few times getting an item to play that has not been
82 // recently played. But do not try too hard, as we don't want
83 // to wait forever.
84 int shuffle_try;
85 int const max_times_to_try = min(4, size());
86 for (shuffle_try = 0; shuffle_try < max_times_to_try; shuffle_try++) {
87 if (!m_previously_played_paths.maybe_contains(next))
88 break;
89
90 m_next_index_to_play = get_random_uniform(size());
91 next = m_model->items().at(m_next_index_to_play).path;
92 }
93 if (shuffle_try == max_times_to_try) {
94 // If we tried too much, maybe it's time to try resetting
95 // the bloom filter and start over.
96 m_previously_played_paths.reset();
97 }
98
99 m_previously_played_paths.add(next);
100 return next;
101}
102
103StringView Playlist::previous()
104{
105 m_next_index_to_play--;
106 if (m_next_index_to_play < 0) {
107 m_next_index_to_play = 0;
108 return {};
109 }
110 return m_model->items().at(m_next_index_to_play).path;
111}