mirror of
https://github.com/Rockbox/rockbox.git
synced 2026-07-10 13:29:52 -04:00
Merge pull request #135 from tsirysndr/copilot/add-http-stream-playback-support
Add HTTP(S) network stream playback to Rockbox Desktop
This commit is contained in:
commit
b48f46dc66
66 changed files with 4129 additions and 241 deletions
61
Cargo.lock
generated
61
Cargo.lock
generated
|
|
@ -574,6 +574,16 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast_node"
|
||||
version = "0.9.9"
|
||||
|
|
@ -1717,6 +1727,15 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
|
|
@ -6843,6 +6862,31 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockito"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"bytes",
|
||||
"colored",
|
||||
"futures-core",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.0",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"similar",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.7"
|
||||
|
|
@ -7003,6 +7047,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netstream"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mockito",
|
||||
"once_cell",
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
|
@ -9082,6 +9136,7 @@ dependencies = [
|
|||
"lazy_static",
|
||||
"local-ip-addr",
|
||||
"md5",
|
||||
"netstream",
|
||||
"owo-colors 4.1.0",
|
||||
"queryst",
|
||||
"rand 0.8.5",
|
||||
|
|
@ -9938,6 +9993,12 @@ version = "0.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ members = [
|
|||
resolver = "2"
|
||||
exclude = ["rmpc", "crates/controls"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Tsiry Sandratraina <tsiry.sndr@fluentci.io"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ menus/audiohw_eq_menu.c
|
|||
#endif
|
||||
menus/eq_menu.c
|
||||
buffering.c
|
||||
streamfd.c
|
||||
voice_thread.c
|
||||
rbcodec_helpers.c
|
||||
menus/main_menu.c
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
*
|
||||
****************************************************************************/
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "system.h"
|
||||
#include "storage.h"
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
#endif
|
||||
#include "buffering.h"
|
||||
#include "linked_list.h"
|
||||
#include "streamfd.h"
|
||||
|
||||
/* Define LOGF_ENABLE to enable logf output in this file */
|
||||
/* #define LOGF_ENABLE */
|
||||
|
|
@ -177,8 +179,8 @@ static struct queue_sender_list buffering_queue_sender_list SHAREDBSS_ATTR;
|
|||
static void close_fd(int *fd_p)
|
||||
{
|
||||
int fd = *fd_p;
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
if (fd != -1) {
|
||||
stream_close(fd);
|
||||
*fd_p = -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -635,18 +637,22 @@ static bool buffer_handle(int handle_id, size_t to_buffer)
|
|||
return true;
|
||||
}
|
||||
|
||||
if (h->fd < 0) { /* file closed, reopen */
|
||||
if (h->fd == -1) { /* file closed, reopen */
|
||||
if (h->path[0] != '\0')
|
||||
h->fd = open(h->path, O_RDONLY);
|
||||
h->fd = stream_open(h->path, O_RDONLY);
|
||||
|
||||
if (h->fd < 0) {
|
||||
fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d path=%s -> fd=%d filesize=%lu end=%lu\n",
|
||||
handle_id, (int)h->type, h->path, h->fd,
|
||||
(unsigned long)h->filesize, (unsigned long)h->end);
|
||||
|
||||
if (h->fd == -1) {
|
||||
/* could not open the file, truncate it where it is */
|
||||
h->filesize = h->end;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (h->start)
|
||||
lseek(h->fd, h->start, SEEK_SET);
|
||||
stream_lseek(h->fd, h->start, SEEK_SET);
|
||||
}
|
||||
|
||||
trigger_cpu_boost();
|
||||
|
|
@ -689,10 +695,18 @@ static bool buffer_handle(int handle_id, size_t to_buffer)
|
|||
return false; /* no space for read */
|
||||
|
||||
/* rc is the actual amount read */
|
||||
ssize_t rc = read(h->fd, ringbuf_ptr(widx), copy_n);
|
||||
ssize_t rc = stream_read(h->fd, ringbuf_ptr(widx), copy_n);
|
||||
|
||||
if (h->end == 0) {
|
||||
/* Log the very first read result for each handle */
|
||||
fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d first_read copy_n=%zd -> rc=%zd fd=%d\n",
|
||||
handle_id, (int)h->type, (ssize_t)copy_n, rc, h->fd);
|
||||
}
|
||||
|
||||
if (rc <= 0) {
|
||||
/* Some kind of filesystem error, maybe recoverable if not codec */
|
||||
fprintf(stderr, "[buffering] buffer_handle: hid=%d type=%d read FAILED rc=%zd end=%lu filesize=%lu\n",
|
||||
handle_id, (int)h->type, rc, (unsigned long)h->end, (unsigned long)h->filesize);
|
||||
if (h->type == TYPE_CODEC) {
|
||||
logf("Partial codec");
|
||||
break;
|
||||
|
|
@ -954,8 +968,8 @@ int bufopen(const char *file, off_t offset, enum data_type type,
|
|||
return ERR_UNSUPPORTED_TYPE;
|
||||
#endif
|
||||
/* Other cases: there is a little more work. */
|
||||
int fd = open(file, O_RDONLY);
|
||||
if (fd < 0)
|
||||
int fd = stream_open(file, O_RDONLY);
|
||||
if (fd == -1)
|
||||
return ERR_FILE_ERROR;
|
||||
|
||||
size_t size = 0;
|
||||
|
|
@ -983,7 +997,7 @@ int bufopen(const char *file, off_t offset, enum data_type type,
|
|||
#endif /* HAVE_ALBUMART */
|
||||
|
||||
if (size == 0)
|
||||
size = filesize(fd);
|
||||
size = (size_t)stream_filesize_fd(fd);
|
||||
|
||||
unsigned int hflags = 0;
|
||||
if (type == TYPE_PACKET_AUDIO || type == TYPE_CODEC)
|
||||
|
|
@ -1005,7 +1019,7 @@ int bufopen(const char *file, off_t offset, enum data_type type,
|
|||
if (!h) {
|
||||
DEBUGF("%s(): failed to add handle\n", __func__);
|
||||
mutex_unlock(&llist_mutex);
|
||||
close(fd);
|
||||
stream_close(fd);
|
||||
|
||||
/*warn playback.c if it is trying to buffer too large of an image*/
|
||||
if(type == TYPE_BITMAP && padded_size >= buffer_len - 64*1024)
|
||||
|
|
@ -1071,7 +1085,7 @@ int bufopen(const char *file, off_t offset, enum data_type type,
|
|||
queue_send(&buffering_queue, Q_BUFFER_HANDLE, handle_id);
|
||||
} else {
|
||||
/* Other types will get buffered in the course of normal operations */
|
||||
close(fd);
|
||||
stream_close(fd);
|
||||
|
||||
if (handle_id >= 0) {
|
||||
/* Inform the buffering thread that we added a handle */
|
||||
|
|
@ -1216,8 +1230,8 @@ static void rebuffer_handle(int handle_id, off_t newpos)
|
|||
h->ridx = h->widx = h->data = new_index;
|
||||
h->start = h->pos = h->end = newpos;
|
||||
|
||||
if (h->fd >= 0)
|
||||
lseek(h->fd, newpos, SEEK_SET);
|
||||
if (h->fd != -1)
|
||||
stream_lseek(h->fd, newpos, SEEK_SET);
|
||||
|
||||
off_t filerem = h->filesize - newpos;
|
||||
bool send = HLIST_NEXT(h) &&
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
#include "settings.h"
|
||||
#include "audiohw.h"
|
||||
#include "general.h"
|
||||
#include "streamfd.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef HAVE_TAGCACHE
|
||||
|
|
@ -1756,13 +1757,23 @@ static bool audio_start_codec(bool auto_skip)
|
|||
|
||||
struct mp3entry *cur_id3 = valid_mp3entry(bufgetid3(info.id3_hid));
|
||||
|
||||
fprintf(stderr, "[playback] audio_start_codec: id3_hid=%d cur_id3=%p\n",
|
||||
info.id3_hid, (void*)cur_id3);
|
||||
|
||||
if (!cur_id3)
|
||||
{
|
||||
fprintf(stderr, "[playback] audio_start_codec: ERROR no valid id3\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[playback] audio_start_codec: path=%s codectype=%u length=%lu elapsed=%lu audio_hid=%d\n",
|
||||
cur_id3->path, cur_id3->codectype, cur_id3->length, cur_id3->elapsed, info.audio_hid);
|
||||
|
||||
buf_pin_handle(info.id3_hid, true);
|
||||
|
||||
if (!audio_init_codec(&info, cur_id3))
|
||||
{
|
||||
fprintf(stderr, "[playback] audio_start_codec: ERROR audio_init_codec failed\n");
|
||||
buf_pin_handle(info.id3_hid, false);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1831,6 +1842,8 @@ static bool audio_start_codec(bool auto_skip)
|
|||
ci.filesize = buf_filesize(info.audio_hid);
|
||||
buf_set_base_handle(info.audio_hid);
|
||||
|
||||
fprintf(stderr, "[playback] audio_start_codec: ci.audio_hid=%d ci.filesize=%lu calling codec_go()\n",
|
||||
ci.audio_hid, (unsigned long)ci.filesize);
|
||||
/* All required data is now available for the codec */
|
||||
codec_go();
|
||||
|
||||
|
|
@ -2147,13 +2160,11 @@ static int audio_load_track(void)
|
|||
if (!path)
|
||||
break;
|
||||
|
||||
/* Test for broken playlists by probing for the files */
|
||||
if (file_exists(path))
|
||||
{
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd >= 0)
|
||||
break;
|
||||
}
|
||||
/* Test for broken playlists by probing for the file/stream. */
|
||||
fd = stream_open(path, O_RDONLY);
|
||||
fprintf(stderr, "[playback] audio_load_track: stream_open(%s) -> fd=%d\n", path, fd);
|
||||
if (fd != -1)
|
||||
break;
|
||||
logf("Open failed %s", path);
|
||||
|
||||
/* only skip if failed track has a successor in playlist */
|
||||
|
|
@ -2194,6 +2205,8 @@ static int audio_load_track(void)
|
|||
if (filling == STATE_FULL ||
|
||||
(info.id3_hid = bufopen(path, 0, TYPE_ID3, NULL)) < 0)
|
||||
{
|
||||
fprintf(stderr, "[playback] audio_load_track: bufopen ID3 failed or buffer full (id3_hid=%d filling=%d)\n",
|
||||
info.id3_hid, filling);
|
||||
/* Buffer or track list is full */
|
||||
struct mp3entry *ub_id3;
|
||||
|
||||
|
|
@ -2202,7 +2215,7 @@ static int audio_load_track(void)
|
|||
/* Load the metadata for the first unbuffered track */
|
||||
ub_id3 = id3_get(UNBUFFERED_ID3);
|
||||
|
||||
if (fd >= 0)
|
||||
if (fd != -1)
|
||||
{
|
||||
id3_mutex_lock();
|
||||
get_metadata(ub_id3, fd, path);
|
||||
|
|
@ -2224,16 +2237,16 @@ static int audio_load_track(void)
|
|||
{
|
||||
track_list_free_info(&info);
|
||||
track_list.in_progress_hid = 0;
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (fd != -1)
|
||||
stream_close(fd);
|
||||
return LOAD_TRACK_ERR_FAILED;
|
||||
}
|
||||
|
||||
/* Successful load initiation */
|
||||
track_list.in_progress_hid = info.self_hid;
|
||||
}
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (fd != -1)
|
||||
stream_close(fd);
|
||||
return LOAD_TRACK_OK;
|
||||
}
|
||||
|
||||
|
|
@ -2264,25 +2277,36 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
|
||||
struct mp3entry *track_id3 = valid_mp3entry(bufgetid3(infop->id3_hid));
|
||||
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: id3_hid=%d track_id3=%p\n",
|
||||
infop->id3_hid, (void*)track_id3);
|
||||
|
||||
if (!track_id3)
|
||||
{
|
||||
/* This is an error condition. Track cannot be played without valid
|
||||
metadata; skip the track. */
|
||||
logf("No metadata");
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: ERROR no valid metadata\n");
|
||||
trackstat = LOAD_TRACK_ERR_FINISH_FAILED;
|
||||
goto audio_finish_load_track_exit;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: path=%s codectype=%u length=%lu elapsed=%lu filesize=%lu first_frame_off=%lu\n",
|
||||
track_id3->path, track_id3->codectype, track_id3->length,
|
||||
track_id3->elapsed, track_id3->FS_PREFIX(filesize),
|
||||
track_id3->first_frame_offset);
|
||||
|
||||
struct track_info user_cur;
|
||||
|
||||
#ifdef HAVE_PLAY_FREQ
|
||||
track_list_user_current(0, &user_cur);
|
||||
bool is_current_user = infop->self_hid == user_cur.self_hid;
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: HAVE_PLAY_FREQ check is_current_user=%d\n", (int)is_current_user);
|
||||
if (audio_auto_change_frequency(track_id3, is_current_user))
|
||||
{
|
||||
// frequency switch requires full re-buffering, so stop buffering
|
||||
filling = STATE_STOPPED;
|
||||
logf("buffering stopped (current_track: %b, current_user: %b)", infop->self_hid == cur_info.self_hid, is_current_user);
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: HAVE_PLAY_FREQ triggered early exit\n");
|
||||
if (is_current_user)
|
||||
// audio_finish_load_track_exit not needed as playback restart is already initiated
|
||||
return trackstat;
|
||||
|
|
@ -2291,17 +2315,21 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
}
|
||||
#endif
|
||||
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: calling audio_load_cuesheet\n");
|
||||
/* Try to load a cuesheet for the track */
|
||||
if (!audio_load_cuesheet(infop, track_id3))
|
||||
{
|
||||
/* No space for cuesheet on buffer, not an error */
|
||||
filling = STATE_FULL;
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: cuesheet buffer full -> exit\n");
|
||||
goto audio_finish_load_track_exit;
|
||||
}
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: cuesheet OK\n");
|
||||
|
||||
#ifdef HAVE_ALBUMART
|
||||
/* Try to load album art for the track */
|
||||
int retval = audio_load_albumart(infop, track_id3, infop->self_hid == cur_info.self_hid);
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: albumart retval=%d\n", retval);
|
||||
if (retval == ERR_BITMAP_TOO_LARGE)
|
||||
{
|
||||
/* No space for album art on buffer because the file is larger than the buffer.
|
||||
|
|
@ -2310,6 +2338,7 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
{
|
||||
/* No space for album art on buffer, not an error */
|
||||
filling = STATE_FULL;
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: albumart buffer full -> exit\n");
|
||||
goto audio_finish_load_track_exit;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -2372,6 +2401,8 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
}
|
||||
|
||||
logf("load track: %s", track_id3->path);
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: audiotype=%d file_offset(before)=%lld\n",
|
||||
audiotype, (long long)file_offset);
|
||||
|
||||
if (file_offset > AUDIO_REBUFFER_GUESS_SIZE)
|
||||
{
|
||||
|
|
@ -2385,7 +2416,11 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
file_offset = track_id3->first_frame_offset;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: bufopen path=%s offset=%lld audiotype=%d\n",
|
||||
track_id3->path, (long long)file_offset, audiotype);
|
||||
|
||||
int hid = bufopen(track_id3->path, file_offset, audiotype, NULL);
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: bufopen -> hid=%d\n", hid);
|
||||
|
||||
if (hid >= 0)
|
||||
{
|
||||
|
|
@ -2427,6 +2462,7 @@ static int audio_finish_load_track(struct track_info *infop)
|
|||
}
|
||||
|
||||
audio_finish_load_track_exit:
|
||||
fprintf(stderr, "[playback] audio_finish_load_track: EXIT trackstat=%d filling=%d\n", trackstat, (int)filling);
|
||||
if (trackstat >= LOAD_TRACK_OK && !track_info_sync(infop))
|
||||
{
|
||||
logf("Track info sync failed");
|
||||
|
|
@ -2584,8 +2620,15 @@ static void audio_on_finish_load_track(int id3_hid)
|
|||
{
|
||||
struct track_info info, user_cur;
|
||||
|
||||
fprintf(stderr, "[playback] audio_on_finish_load_track: id3_hid=%d buf_is_handle=%d\n",
|
||||
id3_hid, buf_is_handle(id3_hid));
|
||||
|
||||
if (!buf_is_handle(id3_hid) || !track_list_last(0, &info))
|
||||
{
|
||||
fprintf(stderr, "[playback] audio_on_finish_load_track: early return (buf_is_handle=%d track_list_last=%d)\n",
|
||||
buf_is_handle(id3_hid), track_list_last(0, &info));
|
||||
return;
|
||||
}
|
||||
|
||||
track_list_user_current(1, &user_cur);
|
||||
if (info.self_hid == user_cur.self_hid)
|
||||
|
|
|
|||
|
|
@ -564,6 +564,22 @@ static ssize_t format_track_path(char *dest, char *src, int buf_length,
|
|||
|
||||
src[len] = '\0';
|
||||
|
||||
/* HTTP(S) URLs are absolute — copy them as-is, no directory prepending.
|
||||
* Also normalise a spurious leading '/' (e.g. "/http://...") that may
|
||||
* have been stored in an older control file by a previous buggy build. */
|
||||
{
|
||||
char *url_src = src;
|
||||
if (*url_src == '/' &&
|
||||
(strncmp(url_src + 1, "http://", 7) == 0 ||
|
||||
strncmp(url_src + 1, "https://", 8) == 0))
|
||||
url_src++;
|
||||
if (strncmp(url_src, "http://", 7) == 0 || strncmp(url_src, "https://", 8) == 0)
|
||||
{
|
||||
strlcpy(dest, url_src, buf_length);
|
||||
return (ssize_t)strlen(dest);
|
||||
}
|
||||
}
|
||||
|
||||
/* Replace backslashes with forward slashes */
|
||||
path_correct_separators(src, src);
|
||||
|
||||
|
|
@ -3024,6 +3040,18 @@ const char* playlist_peek(int steps, char* buf, size_t buf_size)
|
|||
|
||||
temp_ptr = buf;
|
||||
|
||||
/* HTTP(S) URLs don't live on the filesystem — return as-is.
|
||||
* Normalise a spurious leading '/' (e.g. "/http://...") from stale data. */
|
||||
{
|
||||
char *url_ptr = buf;
|
||||
if (*url_ptr == '/' &&
|
||||
(strncmp(url_ptr + 1, "http://", 7) == 0 ||
|
||||
strncmp(url_ptr + 1, "https://", 8) == 0))
|
||||
url_ptr++;
|
||||
if (strncmp(url_ptr, "http://", 7) == 0 || strncmp(url_ptr, "https://", 8) == 0)
|
||||
return url_ptr;
|
||||
}
|
||||
|
||||
/* remove bogus dirs from beginning of path
|
||||
(workaround for buggy playlist creation tools) */
|
||||
while (temp_ptr)
|
||||
|
|
|
|||
126
apps/streamfd.c
Normal file
126
apps/streamfd.c
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/***************************************************************************
|
||||
* Unified stream I/O dispatch for local files and HTTP(S) network streams.
|
||||
* Active in the SDL simulator build and the hosted SDL application build.
|
||||
*
|
||||
* See streamfd.h for the public interface and fd-encoding documentation.
|
||||
***************************************************************************/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
|
||||
#include "streamfd.h"
|
||||
#include "file.h" /* for app_open / app_read / app_lseek / app_close /
|
||||
app_filesize and sim_* equivalents (via macros) */
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* C declarations for the Rust ABI exported by crates/netstream.
|
||||
* ------------------------------------------------------------------ */
|
||||
extern int32_t rb_net_open (const char *url);
|
||||
extern int64_t rb_net_read (int32_t h, void *dst, size_t n);
|
||||
extern int64_t rb_net_lseek (int32_t h, int64_t off, int32_t whence);
|
||||
extern int64_t rb_net_len (int32_t h);
|
||||
extern int64_t rb_net_content_type(int32_t h, char *dst, size_t n);
|
||||
extern void rb_net_close (int32_t h);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Convert an HTTP fd (<=STREAM_HTTP_FD_BASE) back to a Rust handle id. */
|
||||
static inline int32_t http_fd_to_handle(int fd)
|
||||
{
|
||||
return (int32_t)(STREAM_HTTP_FD_BASE - fd);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int path_is_url(const char *path)
|
||||
{
|
||||
return (strncmp(path, "http://", 7) == 0 ||
|
||||
strncmp(path, "https://", 8) == 0);
|
||||
}
|
||||
|
||||
int stream_open(const char *path, int flags)
|
||||
{
|
||||
if (path == NULL)
|
||||
return -1;
|
||||
|
||||
if (path_is_url(path)) {
|
||||
int32_t h = rb_net_open(path);
|
||||
fprintf(stderr, "[streamfd] stream_open(URL): url=%s handle=%d\n", path, (int)h);
|
||||
if (h < 0)
|
||||
return -1;
|
||||
int fd = STREAM_HTTP_FD_BASE - (int)h;
|
||||
fprintf(stderr, "[streamfd] stream_open: url=%s -> http_fd=%d\n", path, fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int fd = open(path, flags);
|
||||
fprintf(stderr, "[streamfd] stream_open(file): path=%s -> fd=%d\n", path, fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
ssize_t stream_read(int fd, void *buf, size_t n)
|
||||
{
|
||||
if (stream_is_http_fd(fd)) {
|
||||
int64_t r = rb_net_read(http_fd_to_handle(fd), buf, n);
|
||||
fprintf(stderr, "[streamfd] stream_read: http_fd=%d n=%zu -> %lld\n", fd, n, (long long)r);
|
||||
return (ssize_t)r;
|
||||
}
|
||||
ssize_t r = read(fd, buf, n);
|
||||
return r;
|
||||
}
|
||||
|
||||
off_t stream_lseek(int fd, off_t off, int whence)
|
||||
{
|
||||
if (stream_is_http_fd(fd)) {
|
||||
int64_t r = rb_net_lseek(http_fd_to_handle(fd), (int64_t)off, whence);
|
||||
fprintf(stderr, "[streamfd] stream_lseek: http_fd=%d off=%lld whence=%d -> %lld\n",
|
||||
fd, (long long)off, whence, (long long)r);
|
||||
return (off_t)r;
|
||||
}
|
||||
return lseek(fd, off, whence);
|
||||
}
|
||||
|
||||
int stream_close(int fd)
|
||||
{
|
||||
if (fd == -1)
|
||||
return 0;
|
||||
if (stream_is_http_fd(fd)) {
|
||||
fprintf(stderr, "[streamfd] stream_close: http_fd=%d (handle=%d)\n",
|
||||
fd, http_fd_to_handle(fd));
|
||||
rb_net_close(http_fd_to_handle(fd));
|
||||
return 0;
|
||||
}
|
||||
return close(fd);
|
||||
}
|
||||
|
||||
off_t stream_filesize_fd(int fd)
|
||||
{
|
||||
if (stream_is_http_fd(fd)) {
|
||||
int64_t len = rb_net_len(http_fd_to_handle(fd));
|
||||
off_t result;
|
||||
if (len < 0) {
|
||||
result = (off_t)0x7FFFFFFF;
|
||||
} else {
|
||||
result = (off_t)len;
|
||||
}
|
||||
fprintf(stderr, "[streamfd] stream_filesize_fd: http_fd=%d -> %lld (raw_len=%lld)\n",
|
||||
fd, (long long)result, (long long)len);
|
||||
return result;
|
||||
}
|
||||
return filesize(fd);
|
||||
}
|
||||
|
||||
ssize_t stream_content_type(int fd, char *buf, size_t n)
|
||||
{
|
||||
if (!stream_is_http_fd(fd))
|
||||
return -1;
|
||||
|
||||
return (ssize_t)rb_net_content_type(http_fd_to_handle(fd), buf, n);
|
||||
}
|
||||
|
||||
#endif /* SIMULATOR || APPLICATION */
|
||||
117
apps/streamfd.h
Normal file
117
apps/streamfd.h
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/***************************************************************************
|
||||
* streamfd.h - Unified stream I/O abstraction for local files and HTTP(S)
|
||||
* network streams.
|
||||
*
|
||||
* In the SDL simulator build and the hosted SDL application build, URLs that
|
||||
* start with "http://" or "https://" are opened as network streams backed by
|
||||
* the Rust "netstream" crate. All other paths are handled by the normal
|
||||
* Rockbox file-system functions.
|
||||
*
|
||||
* File-descriptor encoding (simulator / hosted SDL app):
|
||||
* fd == -1 : closed / unset sentinel
|
||||
* fd >= 0 : normal file descriptor
|
||||
* fd <= STREAM_HTTP_FD_BASE : HTTP stream handle
|
||||
* handle_id = STREAM_HTTP_FD_BASE - fd
|
||||
*
|
||||
* On all other (embedded) builds, every symbol reduces to the existing
|
||||
* Rockbox file-system macro/function so there is zero overhead and zero code
|
||||
* change needed in callers.
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef APPS_STREAMFD_H
|
||||
#define APPS_STREAMFD_H
|
||||
|
||||
#ifdef SIMULATOR
|
||||
#define STREAM_HTTP_ENABLED
|
||||
#elif defined(APPLICATION)
|
||||
#define STREAM_HTTP_ENABLED
|
||||
#endif
|
||||
|
||||
#ifdef STREAM_HTTP_ENABLED
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Sentinel: file descriptors <= this value are HTTP stream handles. */
|
||||
#define STREAM_HTTP_FD_BASE (-1000)
|
||||
|
||||
/** Return non-zero if @p fd refers to an open HTTP stream handle. */
|
||||
static inline int stream_is_http_fd(int fd)
|
||||
{
|
||||
return fd <= STREAM_HTTP_FD_BASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a path.
|
||||
*
|
||||
* If @p path begins with "http://" or "https://" the request is forwarded
|
||||
* to the Rust network layer; otherwise a normal open() is performed.
|
||||
*
|
||||
* @return A file descriptor >= 0, an HTTP handle <= STREAM_HTTP_FD_BASE,
|
||||
* or -1 on error.
|
||||
*/
|
||||
int stream_open(const char *path, int flags);
|
||||
|
||||
/**
|
||||
* Read up to @p n bytes from @p fd into @p buf.
|
||||
* Routes to read() for real fds, rb_net_read() for HTTP fds.
|
||||
*/
|
||||
ssize_t stream_read(int fd, void *buf, size_t n);
|
||||
|
||||
/**
|
||||
* Seek within @p fd.
|
||||
* Routes to lseek() for real fds, rb_net_lseek() for HTTP fds.
|
||||
*/
|
||||
off_t stream_lseek(int fd, off_t off, int whence);
|
||||
|
||||
/**
|
||||
* Close @p fd.
|
||||
* Routes to close() for real fds, rb_net_close() for HTTP fds.
|
||||
* Silently ignores fd == -1.
|
||||
*
|
||||
* @return 0 on success, -1 on error.
|
||||
*/
|
||||
int stream_close(int fd);
|
||||
|
||||
/**
|
||||
* Return the total size of the stream associated with @p fd.
|
||||
*
|
||||
* For HTTP streams: the Content-Length if known, or a large sentinel
|
||||
* value (~2 GiB) if unknown (buffering will truncate on EOF).
|
||||
* For regular fds: delegates to filesize().
|
||||
*
|
||||
* @return Size in bytes, or -1 on error.
|
||||
*/
|
||||
off_t stream_filesize_fd(int fd);
|
||||
|
||||
/**
|
||||
* Copy the normalized Content-Type associated with @p fd into @p buf.
|
||||
*
|
||||
* For HTTP streams this returns the response Content-Type without parameters.
|
||||
* For regular files this returns -1.
|
||||
*
|
||||
* @return Full string length on success, or -1 if unknown/unavailable.
|
||||
*/
|
||||
ssize_t stream_content_type(int fd, char *buf, size_t n);
|
||||
|
||||
#else /* !STREAM_HTTP_ENABLED */
|
||||
|
||||
/*
|
||||
* Non-simulator / embedded builds: map every symbol straight through to
|
||||
* the native Rockbox file-system API so no code needs to change in callers.
|
||||
*/
|
||||
#include "file.h"
|
||||
#include <unistd.h>
|
||||
|
||||
#define stream_is_http_fd(fd) (0)
|
||||
#define stream_open(path, flags) open((path), (flags))
|
||||
#define stream_read(fd, buf, n) read((fd), (buf), (n))
|
||||
#define stream_lseek(fd, off, whence) lseek((fd), (off), (whence))
|
||||
#define stream_close(fd) close(fd)
|
||||
#define stream_filesize_fd(fd) filesize(fd)
|
||||
#define stream_content_type(fd, buf, n) ((ssize_t)-1)
|
||||
|
||||
#endif /* STREAM_HTTP_ENABLED */
|
||||
|
||||
#endif /* APPS_STREAMFD_H */
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Re-link whenever the C/Zig static library changes.
|
||||
println!("cargo:rerun-if-changed=../build-lib/librockbox.a");
|
||||
|
||||
tonic_build::configure()
|
||||
.out_dir("src/api")
|
||||
.file_descriptor_set_path("src/api/rockbox_descriptor.bin")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub mod clear;
|
||||
pub mod community;
|
||||
pub mod login;
|
||||
pub mod open;
|
||||
pub mod repl;
|
||||
pub mod run;
|
||||
pub mod scan;
|
||||
|
|
|
|||
52
cli/src/cmd/open.rs
Normal file
52
cli/src/cmd/open.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use std::{env, path::Path, thread};
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use rockbox::{
|
||||
api::rockbox::v1alpha1::{playback_service_client::PlaybackServiceClient, PlayTrackRequest},
|
||||
install_rockboxd, wait_for_rockboxd,
|
||||
};
|
||||
|
||||
use super::start::start;
|
||||
|
||||
pub async fn open(path_or_url: &str) -> Result<(), Error> {
|
||||
install_rockboxd()?;
|
||||
|
||||
let handle = thread::spawn(|| match start(false) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start Rockbox server: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||
let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string());
|
||||
|
||||
wait_for_rockboxd(port.parse()?, None)?;
|
||||
|
||||
let mut client = PlaybackServiceClient::connect(format!("tcp://{}:{}", host, port)).await?;
|
||||
client
|
||||
.play_track(tonic::Request::new(PlayTrackRequest {
|
||||
path: normalize_path_or_url(path_or_url)?,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
drop(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_path_or_url(path_or_url: &str) -> Result<String, Error> {
|
||||
if path_or_url.starts_with("http://") || path_or_url.starts_with("https://") {
|
||||
return Ok(path_or_url.to_string());
|
||||
}
|
||||
|
||||
let path = Path::new(path_or_url);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Path does not exist: {}", path_or_url));
|
||||
}
|
||||
|
||||
if !path.is_file() {
|
||||
return Err(anyhow!("Path is not a file: {}", path_or_url));
|
||||
}
|
||||
|
||||
Ok(path.canonicalize()?.to_string_lossy().to_string())
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ use clap::{arg, Command};
|
|||
use owo_colors::OwoColorize;
|
||||
|
||||
use cmd::{
|
||||
clear::*, community::*, login::*, repl::*, run::*, scan::*, service, start::*, webui::*,
|
||||
whoami::*,
|
||||
clear::*, community::*, login::*, open::*, repl::*, run::*, scan::*, service, start::*,
|
||||
webui::*, whoami::*,
|
||||
};
|
||||
|
||||
pub mod cmd;
|
||||
|
|
@ -63,6 +63,11 @@ fn cli() -> Command {
|
|||
.about("Run a JavaScript or TypeScript program")
|
||||
.visible_alias("x"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("open")
|
||||
.arg(arg!(<PATH_OR_URL> "Local file path or HTTP URL to play"))
|
||||
.about("Play a local track or remote HTTP URL directly"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("service")
|
||||
.about("Manage systemd service for Rockbox")
|
||||
|
|
@ -127,6 +132,10 @@ async fn main() -> Result<(), Error> {
|
|||
Some(("repl", _)) => {
|
||||
repl();
|
||||
}
|
||||
Some(("open", args)) => {
|
||||
let path_or_url = args.get_one::<String>("PATH_OR_URL").unwrap();
|
||||
open(path_or_url).await?;
|
||||
}
|
||||
Some(("tui", _)) => {
|
||||
rmpc::main_tui()?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Error;
|
||||
use clap::Command;
|
||||
use owo_colors::OwoColorize;
|
||||
use rockbox_library::audio_scan::scan_audio_files;
|
||||
use rockbox_library::audio_scan::{save_audio_metadata, scan_audio_files};
|
||||
use rockbox_library::{create_connection_pool, repo};
|
||||
use rockbox_search::album::Album;
|
||||
use rockbox_search::artist::Artist;
|
||||
|
|
@ -172,3 +172,42 @@ Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn save_remote_track_metadata(url: *const std::ffi::c_char) -> i32 {
|
||||
if url.is_null() {
|
||||
eprintln!("save_remote_track_metadata: null url");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let url = unsafe { CStr::from_ptr(url) };
|
||||
let url = match url.to_str() {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
eprintln!("save_remote_track_metadata: invalid utf-8: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"save_remote_track_metadata: failed to create runtime: {}",
|
||||
e
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
match rt.block_on(async {
|
||||
let pool = create_connection_pool().await?;
|
||||
save_audio_metadata(pool, url).await
|
||||
}) {
|
||||
Ok(()) => 0,
|
||||
Err(e) => {
|
||||
eprintln!("save_remote_track_metadata: {}", e);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ async fn index_graphiql(req: HttpRequest) -> Result<HttpResponse> {
|
|||
|
||||
async fn index_file(req: HttpRequest) -> Result<NamedFile, actix_web::Error> {
|
||||
let id = req.match_info().get("id").unwrap();
|
||||
println!("{}", id);
|
||||
let id = id.split('.').next().unwrap();
|
||||
let mut path = PathBuf::new();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ use anyhow::Error;
|
|||
use lofty::{file::TaggedFileExt, probe::Probe, tag::Accessor};
|
||||
|
||||
pub fn extract_and_save_album_cover(track_path: &str) -> Result<Option<String>, Error> {
|
||||
extract_and_save_album_cover_with_key(track_path, None)
|
||||
}
|
||||
|
||||
pub fn extract_and_save_album_cover_with_key(
|
||||
track_path: &str,
|
||||
album_key: Option<&str>,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let tagged_file = match Probe::open(track_path)
|
||||
.expect("ERROR: Bad path provided!")
|
||||
.read()
|
||||
|
|
@ -15,8 +22,15 @@ pub fn extract_and_save_album_cover(track_path: &str) -> Result<Option<String>,
|
|||
}
|
||||
};
|
||||
|
||||
let primary_tag = tagged_file.primary_tag();
|
||||
let tag = match primary_tag {
|
||||
let tag = match tagged_file
|
||||
.primary_tag()
|
||||
.filter(|tag| !tag.pictures().is_empty())
|
||||
.or_else(|| {
|
||||
tagged_file
|
||||
.tags()
|
||||
.iter()
|
||||
.find(|tag| !tag.pictures().is_empty())
|
||||
}) {
|
||||
Some(tag) => tag,
|
||||
None => {
|
||||
println!("No tag found in file: {}", track_path);
|
||||
|
|
@ -30,13 +44,17 @@ pub fn extract_and_save_album_cover(track_path: &str) -> Result<Option<String>,
|
|||
let covers_path = format!("{}/.config/rockbox.org/covers", home);
|
||||
std::fs::create_dir_all(&covers_path)?;
|
||||
let picture = &pictures[0];
|
||||
|
||||
if tag.album().is_none() {
|
||||
println!("No album found in file: {}", track_path);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let album = md5::compute(tag.album().unwrap().as_bytes());
|
||||
let album_key = tag
|
||||
.album()
|
||||
.filter(|album| !album.trim().is_empty())
|
||||
.map(|album| album.to_string())
|
||||
.or_else(|| {
|
||||
album_key
|
||||
.filter(|album| !album.trim().is_empty())
|
||||
.map(|album| album.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| track_path.to_string());
|
||||
let album = md5::compute(album_key.as_bytes());
|
||||
let filename = format!("{}/{:x}", covers_path, album);
|
||||
match picture.mime_type() {
|
||||
Some(lofty::picture::MimeType::Jpeg) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::album_art::extract_and_save_album_cover;
|
||||
use crate::album_art::extract_and_save_album_cover_with_key;
|
||||
use crate::copyright_message::extract_copyright_message;
|
||||
use crate::entity::album::Album;
|
||||
use crate::entity::album_tracks::AlbumTracks;
|
||||
|
|
@ -6,14 +6,15 @@ use crate::entity::artist::Artist;
|
|||
use crate::entity::artist_tracks::ArtistTracks;
|
||||
use crate::label::extract_label;
|
||||
use crate::{entity::track::Track, repo};
|
||||
use anyhow::Error;
|
||||
use anyhow::{anyhow, Error};
|
||||
use chrono::Utc;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use owo_colors::OwoColorize;
|
||||
use rockbox_sys as rb;
|
||||
use rockbox_sys::types::mp3_entry::Mp3Entry;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use std::path::PathBuf;
|
||||
use std::{io::Write, path::PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
const AUDIO_EXTENSIONS: [&str; 18] = [
|
||||
|
|
@ -65,12 +66,10 @@ pub fn scan_audio_files(
|
|||
}
|
||||
|
||||
pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), Error> {
|
||||
if !AUDIO_EXTENSIONS
|
||||
.into_iter()
|
||||
.any(|ext| path.ends_with(&format!(".{}", ext)))
|
||||
{
|
||||
if !is_supported_audio_path(path) {
|
||||
return Ok(());
|
||||
}
|
||||
let existing_track = repo::track::find_by_path(pool.clone(), path).await?;
|
||||
|
||||
let filename = path.split('/').last().unwrap();
|
||||
let dir = path.replace(filename, "");
|
||||
|
|
@ -80,23 +79,49 @@ pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), E
|
|||
dir,
|
||||
filename.bright_yellow()
|
||||
);
|
||||
let entry = rb::metadata::get_metadata(-1, path);
|
||||
let remote_probe = if is_remote_path(path) {
|
||||
Some(download_partial_remote_file(path).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let metadata_path = remote_probe
|
||||
.as_ref()
|
||||
.map(|probe| probe.path.as_str())
|
||||
.unwrap_or(path);
|
||||
let entry = rb::metadata::get_metadata(-1, metadata_path);
|
||||
let title = track_title(&entry, path);
|
||||
let artist = track_artist(&entry);
|
||||
let album_artist = track_album_artist(&entry, &artist);
|
||||
let album = track_album(&entry, &title);
|
||||
let album_art = extract_and_save_album_cover_with_key(metadata_path, Some(&album))?;
|
||||
|
||||
let track_hash = format!("{:x}", md5::compute(entry.path.as_bytes()));
|
||||
if let Some(existing_track) = existing_track {
|
||||
if let Some(ref album_art) = album_art {
|
||||
if existing_track.album_art.as_deref() != Some(album_art.as_str()) {
|
||||
repo::track::update_album_art(pool.clone(), &existing_track.id, album_art).await?;
|
||||
}
|
||||
|
||||
if let Some(album) = repo::album::find(pool.clone(), &existing_track.album_id).await? {
|
||||
if album.album_art.as_deref() != Some(album_art.as_str()) {
|
||||
repo::album::update_album_art(pool.clone(), &album.id, album_art).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
let track_hash = format!("{:x}", md5::compute(path.as_bytes()));
|
||||
let artist_id = cuid::cuid1()?;
|
||||
let album_id = cuid::cuid1()?;
|
||||
let album_md5 = format!(
|
||||
"{:x}",
|
||||
md5::compute(format!("{}{}{}", entry.albumartist, entry.album, entry.year).as_bytes())
|
||||
md5::compute(format!("{}{}{}", album_artist, album, entry.year).as_bytes())
|
||||
);
|
||||
let artist_id = repo::artist::save(
|
||||
pool.clone(),
|
||||
Artist {
|
||||
id: artist_id.clone(),
|
||||
name: match entry.albumartist.is_empty() {
|
||||
true => entry.artist.clone(),
|
||||
false => entry.albumartist.clone(),
|
||||
},
|
||||
name: artist.clone(),
|
||||
bio: None,
|
||||
image: None,
|
||||
genres: None,
|
||||
|
|
@ -104,23 +129,25 @@ pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), E
|
|||
)
|
||||
.await?;
|
||||
|
||||
let album_art = extract_and_save_album_cover(&entry.path)?;
|
||||
let album_id = repo::album::save(
|
||||
pool.clone(),
|
||||
Album {
|
||||
id: album_id,
|
||||
title: entry.album.clone(),
|
||||
artist: match entry.albumartist.is_empty() {
|
||||
true => entry.artist.clone(),
|
||||
false => entry.albumartist.clone(),
|
||||
},
|
||||
year: entry.year as u32,
|
||||
year_string: entry.year_string.clone(),
|
||||
title: album.clone(),
|
||||
artist: album_artist.clone(),
|
||||
year: clamp_i32_to_u32(entry.year).unwrap_or_default(),
|
||||
year_string: option_string(&entry.year_string).unwrap_or_default(),
|
||||
album_art: album_art.clone(),
|
||||
md5: album_md5,
|
||||
artist_id: artist_id.clone(),
|
||||
label: extract_label(&entry.path)?,
|
||||
copyright_message: extract_copyright_message(&entry.path)?,
|
||||
label: match is_remote_path(path) {
|
||||
true => None,
|
||||
false => extract_label(path)?,
|
||||
},
|
||||
copyright_message: match is_remote_path(path) {
|
||||
true => None,
|
||||
false => extract_copyright_message(path)?,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -129,24 +156,21 @@ pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), E
|
|||
pool.clone(),
|
||||
Track {
|
||||
id: cuid::cuid1()?,
|
||||
path: entry.path.clone(),
|
||||
title: entry.title,
|
||||
artist: entry.artist.clone(),
|
||||
album: entry.album,
|
||||
genre: match entry.genre_string.as_str() {
|
||||
"" => None,
|
||||
_ => Some(entry.genre_string),
|
||||
},
|
||||
year: Some(entry.year as u32),
|
||||
track_number: Some(entry.tracknum as u32),
|
||||
disc_number: entry.discnum as u32,
|
||||
year_string: Some(entry.year_string),
|
||||
path: path.to_string(),
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre: option_string(&entry.genre_string),
|
||||
year: clamp_i32_to_u32(entry.year),
|
||||
track_number: clamp_i32_to_u32(entry.tracknum),
|
||||
disc_number: entry.discnum.max(0) as u32,
|
||||
year_string: option_string(&entry.year_string),
|
||||
composer: entry.composer,
|
||||
album_artist: entry.albumartist.clone(),
|
||||
album_artist,
|
||||
bitrate: entry.bitrate,
|
||||
frequency: entry.frequency as u32,
|
||||
filesize: entry.filesize as u32,
|
||||
length: entry.length as u32,
|
||||
frequency: clamp_u64_to_u32(entry.frequency),
|
||||
filesize: clamp_u64_to_u32(entry.filesize),
|
||||
length: clamp_u64_to_u32(entry.length),
|
||||
md5: track_hash,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
|
|
@ -180,3 +204,189 @@ pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), E
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_remote_path(path: &str) -> bool {
|
||||
path.starts_with("http://") || path.starts_with("https://")
|
||||
}
|
||||
|
||||
fn is_supported_audio_path(path: &str) -> bool {
|
||||
is_remote_path(path)
|
||||
|| AUDIO_EXTENSIONS
|
||||
.into_iter()
|
||||
.any(|ext| path.ends_with(&format!(".{}", ext)))
|
||||
}
|
||||
|
||||
fn option_string(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_i32_to_u32(value: i32) -> Option<u32> {
|
||||
if value > 0 {
|
||||
Some(value as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_u64_to_u32(value: u64) -> u32 {
|
||||
value.min(u32::MAX as u64) as u32
|
||||
}
|
||||
|
||||
fn track_title(entry: &Mp3Entry, path: &str) -> String {
|
||||
if !entry.title.trim().is_empty() {
|
||||
return entry.title.clone();
|
||||
}
|
||||
|
||||
path.rsplit('/')
|
||||
.next()
|
||||
.and_then(|segment| segment.split('?').next())
|
||||
.filter(|segment| !segment.is_empty())
|
||||
.unwrap_or(path)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn track_artist(entry: &Mp3Entry) -> String {
|
||||
if !entry.artist.trim().is_empty() {
|
||||
return entry.artist.clone();
|
||||
}
|
||||
|
||||
if !entry.albumartist.trim().is_empty() {
|
||||
return entry.albumartist.clone();
|
||||
}
|
||||
|
||||
"Unknown Artist".to_string()
|
||||
}
|
||||
|
||||
fn track_album_artist(entry: &Mp3Entry, artist: &str) -> String {
|
||||
if !entry.albumartist.trim().is_empty() {
|
||||
entry.albumartist.clone()
|
||||
} else {
|
||||
artist.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn track_album(entry: &Mp3Entry, title: &str) -> String {
|
||||
if !entry.album.trim().is_empty() {
|
||||
entry.album.clone()
|
||||
} else {
|
||||
title.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteProbeFile {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Drop for RemoteProbeFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_partial_remote_file(url: &str) -> Result<RemoteProbeFile, Error> {
|
||||
const MAX_PROBE_BYTES: usize = 8 * 1024 * 1024;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut response = client
|
||||
.get(url)
|
||||
.header(
|
||||
reqwest::header::RANGE,
|
||||
format!("bytes=0-{}", MAX_PROBE_BYTES - 1),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"failed to probe remote media {}: {}",
|
||||
url,
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let extension = match response.headers().get(reqwest::header::CONTENT_TYPE) {
|
||||
Some(content_type) => match content_type.to_str() {
|
||||
Ok(content_type) => content_type_to_extension(content_type)
|
||||
.or_else(|| extension_from_path(url))
|
||||
.unwrap_or("mp3"),
|
||||
Err(_) => extension_from_path(url).unwrap_or("mp3"),
|
||||
},
|
||||
None => extension_from_path(url).unwrap_or("mp3"),
|
||||
};
|
||||
let probe_path = std::env::temp_dir().join(format!(
|
||||
"rockbox-remote-probe-{:x}.{}",
|
||||
md5::compute(url.as_bytes()),
|
||||
extension
|
||||
));
|
||||
|
||||
let mut file = std::fs::File::create(&probe_path)?;
|
||||
let mut written = 0usize;
|
||||
|
||||
while let Some(chunk) = response.chunk().await? {
|
||||
if written >= MAX_PROBE_BYTES {
|
||||
break;
|
||||
}
|
||||
|
||||
let remaining = MAX_PROBE_BYTES - written;
|
||||
let bytes = if chunk.len() > remaining {
|
||||
&chunk[..remaining]
|
||||
} else {
|
||||
chunk.as_ref()
|
||||
};
|
||||
|
||||
file.write_all(bytes)?;
|
||||
written += bytes.len();
|
||||
}
|
||||
|
||||
if written == 0 {
|
||||
return Err(anyhow!(
|
||||
"failed to probe remote media {}: empty response",
|
||||
url
|
||||
));
|
||||
}
|
||||
|
||||
Ok(RemoteProbeFile {
|
||||
path: probe_path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn extension_from_path(path: &str) -> Option<&str> {
|
||||
let path = path.split('?').next().unwrap_or(path);
|
||||
let extension = path.rsplit('.').next()?.to_ascii_lowercase();
|
||||
AUDIO_EXTENSIONS
|
||||
.into_iter()
|
||||
.find(|candidate| *candidate == extension.as_str())
|
||||
}
|
||||
|
||||
fn content_type_to_extension(content_type: &str) -> Option<&'static str> {
|
||||
match content_type
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap_or(content_type)
|
||||
.trim()
|
||||
{
|
||||
"audio/mpeg" => Some("mp3"),
|
||||
"audio/ogg" => Some("ogg"),
|
||||
"audio/flac" => Some("flac"),
|
||||
"audio/x-m4a" | "audio/m4a" => Some("m4a"),
|
||||
"audio/aac" => Some("aac"),
|
||||
"video/mp4" | "audio/mp4" => Some("mp4"),
|
||||
"audio/wav" | "audio/x-wav" => Some("wav"),
|
||||
"audio/x-wavpack" => Some("wv"),
|
||||
"audio/x-musepack" => Some("mpc"),
|
||||
"audio/aiff" | "audio/x-aiff" => Some("aiff"),
|
||||
"audio/ac3" | "audio/vnd.dolby.dd-raw" => Some("ac3"),
|
||||
"audio/opus" => Some("opus"),
|
||||
"audio/x-speex" => Some("spx"),
|
||||
"audio/prs.sid" => Some("sid"),
|
||||
"audio/ape" | "audio/x-monkeys-audio" => Some("ape"),
|
||||
"audio/x-ms-wma" => Some("wma"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,3 +129,16 @@ pub async fn all(pool: Pool<Sqlite>) -> Result<Vec<Album>, sqlx::Error> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_album_art(
|
||||
pool: Pool<Sqlite>,
|
||||
id: &str,
|
||||
album_art: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("UPDATE album SET album_art = $2 WHERE id = $1")
|
||||
.bind(id)
|
||||
.bind(album_art)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,15 @@ pub async fn all(pool: Pool<Sqlite>) -> Result<Vec<Track>, Error> {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn update_album_art(pool: Pool<Sqlite>, id: &str, album_art: &str) -> Result<(), Error> {
|
||||
sqlx::query("UPDATE track SET album_art = $2 WHERE id = $1")
|
||||
.bind(id)
|
||||
.bind(album_art)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_artist(pool: Pool<Sqlite>, artist: &str) -> Result<Vec<Track>, Error> {
|
||||
let result: Vec<Track> =
|
||||
sqlx::query_as("SELECT * FROM track WHERE artist = $1 ORDER BY title ASC")
|
||||
|
|
|
|||
1584
crates/netstream/Cargo.lock
generated
Normal file
1584
crates/netstream/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
crates/netstream/Cargo.toml
Normal file
16
crates/netstream/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "netstream"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "rbnetstream"
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.5", features = ["blocking", "rustls-tls"], default-features = false }
|
||||
once_cell = "1.17.1"
|
||||
libc = "0.2.168"
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1.7.2"
|
||||
942
crates/netstream/src/lib.rs
Normal file
942
crates/netstream/src/lib.rs
Normal file
|
|
@ -0,0 +1,942 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CStr;
|
||||
use std::io::{self, Read};
|
||||
use std::os::raw::c_char;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Sentinel handle ID returned on error.
|
||||
const INVALID_HANDLE: i32 = -1;
|
||||
|
||||
/// Per-stream state.
|
||||
struct StreamState {
|
||||
url: String,
|
||||
pos: u64,
|
||||
content_length: Option<u64>,
|
||||
content_type: Option<String>,
|
||||
response: Option<reqwest::blocking::Response>,
|
||||
}
|
||||
|
||||
impl StreamState {
|
||||
fn response_content_type(resp: &reqwest::blocking::Response) -> Option<String> {
|
||||
let value = resp
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)?
|
||||
.to_str()
|
||||
.ok()?;
|
||||
let value = value.split(';').next()?.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
fn new(url: String) -> Option<Self> {
|
||||
let response = CLIENT.get(&url).send().ok()?;
|
||||
if !response.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
let content_length = response.content_length();
|
||||
let content_type = Self::response_content_type(&response);
|
||||
Some(StreamState {
|
||||
url,
|
||||
pos: 0,
|
||||
content_length,
|
||||
content_type,
|
||||
response: Some(response),
|
||||
})
|
||||
}
|
||||
|
||||
fn skip_bytes(resp: &mut reqwest::blocking::Response, mut to_skip: u64) -> bool {
|
||||
let mut buf = [0u8; 8192];
|
||||
|
||||
while to_skip > 0 {
|
||||
let chunk = usize::min(to_skip as usize, buf.len());
|
||||
match resp.read(&mut buf[..chunk]) {
|
||||
Ok(0) => return false,
|
||||
Ok(bytes_read) => to_skip -= bytes_read as u64,
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn update_content_length_from_content_range(&mut self, resp: &reqwest::blocking::Response) {
|
||||
if self.content_length.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cr) = resp.headers().get("content-range") {
|
||||
if let Ok(cr_str) = cr.to_str() {
|
||||
if let Some(total_str) = cr_str.split('/').last() {
|
||||
if let Ok(total) = total_str.trim().parse::<u64>() {
|
||||
self.content_length = Some(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_content_range_start(resp: &reqwest::blocking::Response) -> Option<u64> {
|
||||
let value = resp.headers().get("content-range")?.to_str().ok()?;
|
||||
let value = value.strip_prefix("bytes ")?;
|
||||
let (range, _) = value.split_once('/')?;
|
||||
let (start, _) = range.split_once('-')?;
|
||||
start.trim().parse::<u64>().ok()
|
||||
}
|
||||
|
||||
/// Re-issue the request starting at `new_pos` using an HTTP Range header.
|
||||
/// Falls back to reopening from byte 0 and discarding bytes if the server
|
||||
/// ignores Range and responds with the full body.
|
||||
fn seek_to(&mut self, new_pos: u64) -> bool {
|
||||
self.response = None;
|
||||
let result = CLIENT
|
||||
.get(&self.url)
|
||||
.header("Range", format!("bytes={}-", new_pos))
|
||||
.send();
|
||||
|
||||
match result {
|
||||
Ok(resp) if resp.status().as_u16() == 206 => {
|
||||
self.update_content_length_from_content_range(&resp);
|
||||
if self.content_type.is_none() {
|
||||
self.content_type = Self::response_content_type(&resp);
|
||||
}
|
||||
|
||||
if Self::parse_content_range_start(&resp) != Some(new_pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.response = Some(resp);
|
||||
self.pos = new_pos;
|
||||
true
|
||||
}
|
||||
Ok(mut resp) if resp.status().is_success() => {
|
||||
if self.content_length.is_none() {
|
||||
self.content_length = resp.content_length();
|
||||
}
|
||||
if self.content_type.is_none() {
|
||||
self.content_type = Self::response_content_type(&resp);
|
||||
}
|
||||
|
||||
if new_pos > 0 && !Self::skip_bytes(&mut resp, new_pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.response = Some(resp);
|
||||
self.pos = new_pos;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_as_file<R: Read>(reader: &mut R, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut total = 0;
|
||||
|
||||
while total < buf.len() {
|
||||
match reader.read(&mut buf[total..]) {
|
||||
Ok(0) => break,
|
||||
Ok(bytes_read) => total += bytes_read,
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(err) => {
|
||||
if total > 0 {
|
||||
break;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
static STREAMS: Lazy<Mutex<HashMap<i32, Arc<Mutex<StreamState>>>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
static CLIENT: Lazy<reqwest::blocking::Client> = Lazy::new(|| {
|
||||
reqwest::blocking::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.expect("failed to build global HTTP client")
|
||||
});
|
||||
|
||||
static NEXT_HANDLE: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Public C ABI
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// Open a URL and return an integer handle, or -1 on failure.
|
||||
///
|
||||
/// # Safety
|
||||
/// `url` must be a valid, NUL-terminated C string.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rb_net_open(url: *const c_char) -> i32 {
|
||||
if url.is_null() {
|
||||
return INVALID_HANDLE;
|
||||
}
|
||||
let url_str = match CStr::from_ptr(url).to_str() {
|
||||
Ok(s) => s.to_owned(),
|
||||
Err(_) => return INVALID_HANDLE,
|
||||
};
|
||||
|
||||
let state = match StreamState::new(url_str.clone()) {
|
||||
Some(s) => {
|
||||
eprintln!(
|
||||
"[netstream] rb_net_open: url={} content_length={:?} content_type={:?}",
|
||||
url_str, s.content_length, s.content_type
|
||||
);
|
||||
s
|
||||
}
|
||||
None => {
|
||||
eprintln!("[netstream] rb_net_open: FAILED url={}", url_str);
|
||||
return INVALID_HANDLE;
|
||||
}
|
||||
};
|
||||
|
||||
let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst);
|
||||
STREAMS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(handle, Arc::new(Mutex::new(state)));
|
||||
eprintln!(
|
||||
"[netstream] rb_net_open: url={} -> handle={}",
|
||||
url_str, handle
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
/// Read up to `n` bytes from stream `h` into `dst`.
|
||||
/// Returns the number of bytes read, 0 on EOF, or -1 on error.
|
||||
///
|
||||
/// # Safety
|
||||
/// `dst` must point to a buffer of at least `n` bytes.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rb_net_read(h: i32, dst: *mut libc::c_void, n: libc::size_t) -> i64 {
|
||||
if dst.is_null() || n == 0 {
|
||||
return 0;
|
||||
}
|
||||
// Acquire the global map lock only long enough to clone the per-handle Arc,
|
||||
// then release it so other handles can proceed concurrently.
|
||||
let handle_arc = {
|
||||
let streams = STREAMS.lock().unwrap();
|
||||
match streams.get(&h) {
|
||||
Some(arc) => arc.clone(),
|
||||
None => return -1,
|
||||
}
|
||||
};
|
||||
let mut state = handle_arc.lock().unwrap();
|
||||
let pos_before = state.pos;
|
||||
let resp = match &mut state.response {
|
||||
Some(r) => r,
|
||||
None => return -1,
|
||||
};
|
||||
let buf = std::slice::from_raw_parts_mut(dst as *mut u8, n);
|
||||
match read_as_file(resp, buf) {
|
||||
Ok(bytes_read) => {
|
||||
state.pos += bytes_read as u64;
|
||||
eprintln!(
|
||||
"[netstream] rb_net_read: h={} n={} pos_before={} -> read={} pos_after={}",
|
||||
h, n, pos_before, bytes_read, state.pos
|
||||
);
|
||||
bytes_read as i64
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[netstream] rb_net_read: h={} n={} pos={} -> ERROR {:?}",
|
||||
h, n, pos_before, e
|
||||
);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Seek within stream `h`. `whence` follows POSIX semantics:
|
||||
/// 0 = SEEK_SET, 1 = SEEK_CUR, 2 = SEEK_END.
|
||||
/// Returns the new position on success, or -1 on failure.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rb_net_lseek(h: i32, off: i64, whence: libc::c_int) -> i64 {
|
||||
const SEEK_SET: libc::c_int = 0;
|
||||
const SEEK_CUR: libc::c_int = 1;
|
||||
const SEEK_END: libc::c_int = 2;
|
||||
|
||||
// Acquire the global map lock only long enough to clone the per-handle Arc,
|
||||
// then release it so other handles can proceed concurrently.
|
||||
let handle_arc = {
|
||||
let streams = STREAMS.lock().unwrap();
|
||||
match streams.get(&h) {
|
||||
Some(arc) => arc.clone(),
|
||||
None => return -1,
|
||||
}
|
||||
};
|
||||
let mut state = handle_arc.lock().unwrap();
|
||||
|
||||
let new_pos: u64 = match whence {
|
||||
x if x == SEEK_SET => {
|
||||
if off < 0 {
|
||||
return -1;
|
||||
}
|
||||
off as u64
|
||||
}
|
||||
x if x == SEEK_CUR => {
|
||||
if off < 0 {
|
||||
let abs_off = (-off) as u64;
|
||||
if abs_off > state.pos {
|
||||
return -1;
|
||||
}
|
||||
state.pos - abs_off
|
||||
} else {
|
||||
state.pos + off as u64
|
||||
}
|
||||
}
|
||||
x if x == SEEK_END => {
|
||||
let len = match state.content_length {
|
||||
Some(l) => l,
|
||||
None => return -1,
|
||||
};
|
||||
if off > 0 {
|
||||
return -1;
|
||||
}
|
||||
let abs_off = (-off) as u64;
|
||||
if abs_off > len {
|
||||
return -1;
|
||||
}
|
||||
len - abs_off
|
||||
}
|
||||
_ => return -1,
|
||||
};
|
||||
|
||||
// Fast-path: already there (no need to restart the request).
|
||||
if new_pos == state.pos {
|
||||
eprintln!(
|
||||
"[netstream] rb_net_lseek: h={} off={} whence={} -> already at pos={} (no-op)",
|
||||
h, off, whence, state.pos
|
||||
);
|
||||
return state.pos as i64;
|
||||
}
|
||||
|
||||
let old_pos = state.pos;
|
||||
if state.seek_to(new_pos) {
|
||||
eprintln!(
|
||||
"[netstream] rb_net_lseek: h={} off={} whence={} old_pos={} -> new_pos={}",
|
||||
h, off, whence, old_pos, state.pos
|
||||
);
|
||||
state.pos as i64
|
||||
} else {
|
||||
eprintln!(
|
||||
"[netstream] rb_net_lseek: h={} off={} whence={} old_pos={} -> FAILED",
|
||||
h, off, whence, old_pos
|
||||
);
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the total content length of stream `h`, or -1 if unknown.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rb_net_len(h: i32) -> i64 {
|
||||
let handle_arc = {
|
||||
let streams = STREAMS.lock().unwrap();
|
||||
match streams.get(&h) {
|
||||
Some(arc) => arc.clone(),
|
||||
None => return -1,
|
||||
}
|
||||
};
|
||||
let len = handle_arc
|
||||
.lock()
|
||||
.unwrap()
|
||||
.content_length
|
||||
.map(|l| l as i64)
|
||||
.unwrap_or(-1);
|
||||
eprintln!("[netstream] rb_net_len: h={} -> {}", h, len);
|
||||
len
|
||||
}
|
||||
|
||||
/// Copy the normalized Content-Type for stream `h` into `dst`.
|
||||
/// Returns the full string length on success, or -1 if unavailable.
|
||||
///
|
||||
/// # Safety
|
||||
/// `dst` must point to a writable buffer of at least `n` bytes when `n > 0`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rb_net_content_type(h: i32, dst: *mut c_char, n: libc::size_t) -> i64 {
|
||||
let handle_arc = {
|
||||
let streams = STREAMS.lock().unwrap();
|
||||
match streams.get(&h) {
|
||||
Some(arc) => arc.clone(),
|
||||
None => return -1,
|
||||
}
|
||||
};
|
||||
let state = handle_arc.lock().unwrap();
|
||||
let content_type = match state.content_type.as_deref() {
|
||||
Some(value) => value,
|
||||
None => return -1,
|
||||
};
|
||||
|
||||
if !dst.is_null() && n > 0 {
|
||||
let bytes = content_type.as_bytes();
|
||||
let copy_len = usize::min(bytes.len(), n.saturating_sub(1));
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst as *mut u8, copy_len);
|
||||
*dst.add(copy_len) = 0;
|
||||
}
|
||||
|
||||
content_type.len() as i64
|
||||
}
|
||||
|
||||
/// Close stream `h` and release its resources.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rb_net_close(h: i32) {
|
||||
eprintln!("[netstream] rb_net_close: h={}", h);
|
||||
STREAMS.lock().unwrap().remove(&h);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Tests
|
||||
// ------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mockito::Matcher;
|
||||
use std::ffi::CString;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Helper: build a NUL-terminated C URL string for a path on the mock server.
|
||||
fn c_url(server: &mockito::Server, path: &str) -> CString {
|
||||
CString::new(format!("{}{}", server.url(), path)).unwrap()
|
||||
}
|
||||
|
||||
struct PartialReader {
|
||||
inner: Cursor<Vec<u8>>,
|
||||
chunk_size: usize,
|
||||
}
|
||||
|
||||
impl PartialReader {
|
||||
fn new(data: &[u8], chunk_size: usize) -> Self {
|
||||
Self {
|
||||
inner: Cursor::new(data.to_vec()),
|
||||
chunk_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PartialReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let chunk = usize::min(self.chunk_size, buf.len());
|
||||
self.inner.read(&mut buf[..chunk])
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Open / close
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// Opening a valid URL returns a non-negative handle.
|
||||
#[test]
|
||||
fn test_open_and_close() {
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/audio.mp3")
|
||||
.with_status(200)
|
||||
.with_header("content-length", "4")
|
||||
.with_body(b"data")
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/audio.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0, "open should return a valid handle");
|
||||
|
||||
rb_net_close(handle);
|
||||
// After close, rb_net_len should return -1 (unknown handle).
|
||||
assert_eq!(
|
||||
rb_net_len(handle),
|
||||
-1,
|
||||
"closed handle should return -1 from rb_net_len"
|
||||
);
|
||||
}
|
||||
|
||||
/// Passing a null pointer returns INVALID_HANDLE.
|
||||
#[test]
|
||||
fn test_open_null_url() {
|
||||
let handle = unsafe { rb_net_open(std::ptr::null()) };
|
||||
assert_eq!(handle, INVALID_HANDLE);
|
||||
}
|
||||
|
||||
/// Connecting to a port where nothing is listening returns INVALID_HANDLE.
|
||||
#[test]
|
||||
fn test_open_unreachable_host() {
|
||||
// Port 19998 is extremely unlikely to be in use.
|
||||
let url = CString::new("http://127.0.0.1:19998/file.mp3").unwrap();
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert_eq!(
|
||||
handle, INVALID_HANDLE,
|
||||
"unreachable server should return INVALID_HANDLE"
|
||||
);
|
||||
}
|
||||
|
||||
/// A 404 response causes rb_net_open to return INVALID_HANDLE.
|
||||
#[test]
|
||||
fn test_open_404() {
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server.mock("GET", "/missing.mp3").with_status(404).create();
|
||||
|
||||
let url = c_url(&server, "/missing.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert_eq!(
|
||||
handle, INVALID_HANDLE,
|
||||
"404 response should return INVALID_HANDLE"
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Content-Length / rb_net_len
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// rb_net_len returns the Content-Length when the server provides it.
|
||||
#[test]
|
||||
fn test_known_content_length() {
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/known.mp3")
|
||||
.with_status(200)
|
||||
.with_header("content-length", "1234")
|
||||
.with_body(vec![0u8; 1234])
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/known.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
assert_eq!(rb_net_len(handle), 1234);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type_is_available() {
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/typed")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "audio/m4a; charset=binary")
|
||||
.with_body("data")
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/typed");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let mut buf = vec![0i8; 32];
|
||||
let n = unsafe { rb_net_content_type(handle, buf.as_mut_ptr(), buf.len()) };
|
||||
assert_eq!(n, "audio/m4a".len() as i64);
|
||||
|
||||
let content_type = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap();
|
||||
assert_eq!(content_type, "audio/m4a");
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// rb_net_len returns the value from the Content-Length response header.
|
||||
#[test]
|
||||
fn test_unknown_content_length() {
|
||||
// Note: mockito automatically sets a content-length header from the body
|
||||
// length when serving responses, so we verify here that rb_net_len
|
||||
// correctly reads whatever content-length the server sends.
|
||||
let body: &[u8] = b"some data";
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/stream.mp3")
|
||||
.with_status(200)
|
||||
.with_body(body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/stream.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
// mockito sets content-length = body.len() when no explicit header is given.
|
||||
let len = rb_net_len(handle);
|
||||
assert_eq!(
|
||||
len,
|
||||
body.len() as i64,
|
||||
"rb_net_len should reflect the server's content-length"
|
||||
);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reading
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// rb_net_read returns the expected bytes.
|
||||
#[test]
|
||||
fn test_read_bytes() {
|
||||
let body: &[u8] = b"Hello, Rockbox!";
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/song.mp3")
|
||||
.with_status(200)
|
||||
.with_body(body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/song.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let mut buf = vec![0u8; 64];
|
||||
let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(n, body.len() as i64);
|
||||
assert_eq!(&buf[..n as usize], body);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_as_file_retries_partial_reads() {
|
||||
let mut reader = PartialReader::new(b"Hello, Rockbox!", 3);
|
||||
let mut buf = vec![0u8; 15];
|
||||
|
||||
let n = read_as_file(&mut reader, &mut buf).unwrap();
|
||||
|
||||
assert_eq!(n, 15);
|
||||
assert_eq!(&buf, b"Hello, Rockbox!");
|
||||
}
|
||||
|
||||
/// rb_net_read returns 0 at EOF (after all bytes have been consumed).
|
||||
#[test]
|
||||
fn test_read_eof() {
|
||||
let body: &[u8] = b"tiny";
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/eof.mp3")
|
||||
.with_status(200)
|
||||
.with_body(body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/eof.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let mut buf = vec![0u8; 1024];
|
||||
// First read drains the body.
|
||||
let n1 = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(n1, body.len() as i64);
|
||||
|
||||
// Second read should signal EOF.
|
||||
let n2 = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(n2, 0, "second read should return 0 at EOF");
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// rb_net_read on an unknown handle returns -1.
|
||||
#[test]
|
||||
fn test_read_invalid_handle() {
|
||||
let mut buf = vec![0u8; 16];
|
||||
let result =
|
||||
unsafe { rb_net_read(i32::MAX, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(result, -1, "read on unknown handle should return -1");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Seeking
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// SEEK_SET re-issues a Range request and returns the new position.
|
||||
#[test]
|
||||
fn test_seek_set_range_request() {
|
||||
let full_body: &[u8] = b"0123456789ABCDEF"; // 16 bytes
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
// Initial GET (no Range header).
|
||||
let _initial = server
|
||||
.mock("GET", "/seekable.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "16")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
// Range request from byte 8.
|
||||
let _range = server
|
||||
.mock("GET", "/seekable.mp3")
|
||||
.match_header("range", "bytes=8-")
|
||||
.with_status(206)
|
||||
.with_header("content-range", "bytes 8-15/16")
|
||||
.with_header("content-length", "8")
|
||||
.with_body(&full_body[8..])
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/seekable.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let new_pos = rb_net_lseek(handle, 8, libc::SEEK_SET);
|
||||
assert_eq!(new_pos, 8, "SEEK_SET(8) should return position 8");
|
||||
|
||||
// Read the remaining 8 bytes and verify they match the tail of full_body.
|
||||
let mut buf = vec![0u8; 16];
|
||||
let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(n, 8);
|
||||
assert_eq!(&buf[..8], &full_body[8..]);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// SEEK_CUR(0) is a no-op that queries the current position without a new request.
|
||||
#[test]
|
||||
fn test_seek_cur_no_op() {
|
||||
let body: &[u8] = b"ABCDEFGHIJ"; // 10 bytes
|
||||
let mut server = mockito::Server::new();
|
||||
let _mock = server
|
||||
.mock("GET", "/cur.mp3")
|
||||
.with_status(200)
|
||||
.with_header("content-length", "10")
|
||||
.with_body(body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/cur.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
// Read 5 bytes → position advances to 5.
|
||||
let mut buf = vec![0u8; 5];
|
||||
let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, 5) };
|
||||
assert_eq!(n, 5);
|
||||
|
||||
// SEEK_CUR(0) should return current position without a new HTTP request.
|
||||
let pos = rb_net_lseek(handle, 0, libc::SEEK_CUR);
|
||||
assert_eq!(pos, 5, "SEEK_CUR(0) should return current position");
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// SEEK_END(-2) on a 10-byte file should yield position 8.
|
||||
#[test]
|
||||
fn test_seek_end() {
|
||||
let full_body: &[u8] = b"XXXXXXXXXX"; // 10 bytes
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
let _initial = server
|
||||
.mock("GET", "/end.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "10")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
let _range = server
|
||||
.mock("GET", "/end.mp3")
|
||||
.match_header("range", "bytes=8-")
|
||||
.with_status(206)
|
||||
.with_header("content-range", "bytes 8-9/10")
|
||||
.with_body(&full_body[8..])
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/end.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let pos = rb_net_lseek(handle, -2, libc::SEEK_END);
|
||||
assert_eq!(
|
||||
pos, 8,
|
||||
"SEEK_END(-2) on 10-byte file should give position 8"
|
||||
);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// SEEK_END on a stream with unknown Content-Length returns -1.
|
||||
/// This tests the graceful failure path for SEEK_END.
|
||||
#[test]
|
||||
fn test_seek_end_unknown_length() {
|
||||
// We use a 416 mock to trigger the failure in seek_to; this also tests
|
||||
// the seek failure path for SEEK_END when content-length becomes
|
||||
// unavailable (e.g. after a failed Range response cleared the state).
|
||||
let full_body: &[u8] = b"data";
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
// Initial GET: explicitly provide no content-length so SEEK_END has nothing.
|
||||
// We do this by setting content-length to 0 on a 200 response without body,
|
||||
// then testing SEEK_END with a negative offset.
|
||||
let _mock = server
|
||||
.mock("GET", "/nosize.mp3")
|
||||
.with_status(200)
|
||||
// mockito sets content-length from body; use a large body to get a real length,
|
||||
// then test that SEEK_END(-offset > length) correctly fails.
|
||||
.with_header("content-length", "4")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/nosize.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
// Seeking past the beginning is invalid: SEEK_END with |offset| > length.
|
||||
let pos = rb_net_lseek(handle, -100, libc::SEEK_END);
|
||||
assert_eq!(
|
||||
pos, -1,
|
||||
"SEEK_END with offset beyond file start should return -1"
|
||||
);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// When the server returns 416 for a Range request, seek fails gracefully.
|
||||
#[test]
|
||||
fn test_seek_range_not_supported() {
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
// Initial GET succeeds.
|
||||
let _initial = server
|
||||
.mock("GET", "/noseek.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "100")
|
||||
.with_body(vec![0u8; 100])
|
||||
.create();
|
||||
|
||||
// Range request returns "416 Range Not Satisfiable".
|
||||
let _no_range = server
|
||||
.mock("GET", "/noseek.mp3")
|
||||
.match_header("range", Matcher::Any)
|
||||
.with_status(416)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/noseek.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let result = rb_net_lseek(handle, 50, libc::SEEK_SET);
|
||||
assert_eq!(
|
||||
result, -1,
|
||||
"seek should fail gracefully when Range is not supported"
|
||||
);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// If the server ignores Range and returns 200 with the full body, seek
|
||||
/// still succeeds by discarding bytes until the requested position.
|
||||
#[test]
|
||||
fn test_seek_falls_back_when_range_is_ignored() {
|
||||
let full_body: &[u8] = b"0123456789ABCDEF";
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
let _initial = server
|
||||
.mock("GET", "/ignore-range.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "16")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
let _ignored_range = server
|
||||
.mock("GET", "/ignore-range.mp3")
|
||||
.match_header("range", "bytes=8-")
|
||||
.with_status(200)
|
||||
.with_header("content-length", "16")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/ignore-range.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let new_pos = rb_net_lseek(handle, 8, libc::SEEK_SET);
|
||||
assert_eq!(new_pos, 8, "seek should land at byte 8 even without 206");
|
||||
|
||||
let mut buf = vec![0u8; 16];
|
||||
let n = unsafe { rb_net_read(handle, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
assert_eq!(n, 8);
|
||||
assert_eq!(&buf[..8], &full_body[8..]);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// A malformed 206 response that does not start at the requested offset
|
||||
/// must fail instead of silently desynchronizing the stream position.
|
||||
#[test]
|
||||
fn test_seek_rejects_wrong_content_range_start() {
|
||||
let full_body: &[u8] = b"0123456789ABCDEF";
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
let _initial = server
|
||||
.mock("GET", "/bad-range.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "16")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
let _bad_range = server
|
||||
.mock("GET", "/bad-range.mp3")
|
||||
.match_header("range", "bytes=8-")
|
||||
.with_status(206)
|
||||
.with_header("content-range", "bytes 0-7/16")
|
||||
.with_body(&full_body[..8])
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/bad-range.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
let result = rb_net_lseek(handle, 8, libc::SEEK_SET);
|
||||
assert_eq!(result, -1, "mismatched Content-Range should fail");
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
|
||||
/// Content-Range header from a 206 response populates content_length
|
||||
/// when it was not provided in the initial response.
|
||||
/// We verify here that after seeking, the total length is available.
|
||||
#[test]
|
||||
fn test_content_length_from_content_range() {
|
||||
let full_body: &[u8] = b"0123456789"; // 10 bytes
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
// Initial GET: content-length matches body (10).
|
||||
let _initial = server
|
||||
.mock("GET", "/range-len.mp3")
|
||||
.match_header("range", Matcher::Missing)
|
||||
.with_status(200)
|
||||
.with_header("content-length", "10")
|
||||
.with_body(full_body)
|
||||
.create();
|
||||
|
||||
// Range request: 206 includes Content-Range which also reveals total size.
|
||||
let _range = server
|
||||
.mock("GET", "/range-len.mp3")
|
||||
.match_header("range", "bytes=5-")
|
||||
.with_status(206)
|
||||
.with_header("content-range", "bytes 5-9/10")
|
||||
.with_body(&full_body[5..])
|
||||
.create();
|
||||
|
||||
let url = c_url(&server, "/range-len.mp3");
|
||||
let handle = unsafe { rb_net_open(url.as_ptr()) };
|
||||
assert!(handle >= 0);
|
||||
|
||||
// Total length is known from the initial response.
|
||||
assert_eq!(
|
||||
rb_net_len(handle),
|
||||
10,
|
||||
"length should be known from initial response"
|
||||
);
|
||||
|
||||
// Seeking causes a 206 response whose Content-Range also confirms the total length.
|
||||
let pos = rb_net_lseek(handle, 5, libc::SEEK_SET);
|
||||
assert_eq!(pos, 5, "seek should succeed");
|
||||
|
||||
// Length is still correctly reported after seek.
|
||||
assert_eq!(
|
||||
rb_net_len(handle),
|
||||
10,
|
||||
"length should remain correct after seek"
|
||||
);
|
||||
|
||||
rb_net_close(handle);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ fn get_file_extension(mime: &str) -> Result<&str, Error> {
|
|||
"audio/ogg" => Ok("ogg"),
|
||||
"audio/flac" => Ok("flac"),
|
||||
"audio/x-m4a" => Ok("m4a"),
|
||||
"audio/m4a" => Ok("m4a"),
|
||||
"audio/aac" => Ok("aac"),
|
||||
"video/mp4" => Ok("mp4"),
|
||||
"audio/wav" => Ok("wav"),
|
||||
|
|
|
|||
|
|
@ -208,7 +208,12 @@ impl PlaybackService for Playback {
|
|||
.map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||
if let Some(metadata) = metadata {
|
||||
let mut track = track.clone();
|
||||
track.album_art = metadata.album_art;
|
||||
// Only override album_art if the DB has a non-None value; the HTTP
|
||||
// endpoint already resolved album_art via find_internal_track_by_url,
|
||||
// so we must not overwrite it with None from the remote-saved record.
|
||||
if metadata.album_art.is_some() {
|
||||
track.album_art = metadata.album_art;
|
||||
}
|
||||
track.album_id = Some(metadata.album_id);
|
||||
track.artist_id = Some(metadata.artist_id);
|
||||
return Ok(tonic::Response::new(track.into()));
|
||||
|
|
@ -594,6 +599,7 @@ impl PlaybackService for Playback {
|
|||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())
|
||||
.map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -602,6 +608,7 @@ impl PlaybackService for Playback {
|
|||
.put(&url)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())
|
||||
.map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(tonic::Response::new(PlayTrackResponse::default()))
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ rockbox-network = { path = "../network" }
|
|||
rockbox-rpc = {path = "../rpc"}
|
||||
rockbox-search = {path = "../search"}
|
||||
rockbox-settings = {path = "../settings"}
|
||||
netstream = { path = "../netstream" }
|
||||
rockbox-sys = {path = "../sys"}
|
||||
rockbox-tracklist = {path = "../tracklist"}
|
||||
rockbox-traits = {path = "../traits"}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::env;
|
||||
use std::{env, ffi::CString};
|
||||
|
||||
use crate::PLAYER_MUTEX;
|
||||
use crate::{
|
||||
http::{Context, Request, Response},
|
||||
GLOBAL_MUTEX,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use anyhow::{anyhow, Error};
|
||||
use local_ip_addr::get_local_ip_address;
|
||||
use rand::seq::SliceRandom;
|
||||
use rockbox_chromecast::Chromecast;
|
||||
|
|
@ -17,6 +17,10 @@ use rockbox_sys::{
|
|||
use rockbox_traits::types::track::Track;
|
||||
use rockbox_types::{device::Device, LoadTracks, NewVolume};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn save_remote_track_metadata(url: *const std::ffi::c_char) -> i32;
|
||||
}
|
||||
|
||||
pub async fn load(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> {
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
let mut player = ctx.player.lock().unwrap();
|
||||
|
|
@ -42,16 +46,36 @@ pub async fn load(ctx: &Context, req: &Request, res: &mut Response) -> Result<()
|
|||
let req_body = req.body.as_ref().unwrap();
|
||||
let request: LoadTracks = serde_json::from_str(&req_body)?;
|
||||
|
||||
for path in &request.tracks {
|
||||
if path.starts_with("http://") || path.starts_with("https://") {
|
||||
ensure_remote_track_metadata(path.clone()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let rockbox_addr = env::var("ROCKBOX_ADDR").unwrap_or_else(|_| get_local_ip_address().unwrap());
|
||||
let rockbox_port = env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or_else(|_| "6062".to_string());
|
||||
let kv = ctx.kv.lock().unwrap();
|
||||
let mut tracks = request
|
||||
.tracks
|
||||
.iter()
|
||||
.filter(|t| kv.get(*t).is_some())
|
||||
.map(|t| {
|
||||
let track = kv.get(t).unwrap();
|
||||
Track {
|
||||
let mut tracks = Vec::new();
|
||||
|
||||
for requested_path in &request.tracks {
|
||||
let track = {
|
||||
let kv = ctx.kv.lock().unwrap();
|
||||
kv.get(requested_path).cloned()
|
||||
};
|
||||
|
||||
let track = match track {
|
||||
Some(track) => Some(track),
|
||||
None => {
|
||||
let track = repo::track::find_by_path(ctx.pool.clone(), requested_path).await?;
|
||||
if let Some(ref track) = track {
|
||||
let mut kv = ctx.kv.lock().unwrap();
|
||||
kv.set(requested_path, track.clone());
|
||||
}
|
||||
track
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(track) = track {
|
||||
tracks.push(Track {
|
||||
id: track.id.clone(),
|
||||
title: track.title.clone(),
|
||||
artist: track.artist.clone(),
|
||||
|
|
@ -71,9 +95,15 @@ pub async fn load(ctx: &Context, req: &Request, res: &mut Response) -> Result<()
|
|||
disc_number: track.disc_number,
|
||||
duration: Some(track.length as f32 / 1000.0),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Track>>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if tracks.is_empty() {
|
||||
res.set_status(404);
|
||||
res.text("No playable tracks found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if Some(true) == request.shuffle {
|
||||
tracks.shuffle(&mut rand::thread_rng());
|
||||
|
|
@ -169,7 +199,28 @@ pub async fn current_track(ctx: &Context, _req: &Request, res: &mut Response) ->
|
|||
|
||||
if let Some(player) = player.as_deref_mut() {
|
||||
let current_playback = player.get_current_playback().await?;
|
||||
let track: Option<Mp3Entry> = current_playback.current_track.map(|t| t.into());
|
||||
let mut track: Option<Mp3Entry> = current_playback.current_track.map(|mut t| {
|
||||
if t.path.is_empty() {
|
||||
t.path = t.uri.clone();
|
||||
}
|
||||
t.into()
|
||||
});
|
||||
|
||||
if let Some(lookup_path) = track
|
||||
.as_ref()
|
||||
.map(|t| t.path.clone())
|
||||
.filter(|path| !path.is_empty())
|
||||
{
|
||||
let metadata = find_track_metadata(ctx, &lookup_path).await?;
|
||||
if let Some(metadata) = metadata {
|
||||
let current_track = track.as_mut().unwrap();
|
||||
current_track.id = Some(metadata.id);
|
||||
current_track.album_art = metadata.album_art.or(current_track.album_art.clone());
|
||||
current_track.album_id = Some(metadata.album_id);
|
||||
current_track.artist_id = Some(metadata.artist_id);
|
||||
}
|
||||
}
|
||||
|
||||
let track = track.map(|mut t| {
|
||||
t.elapsed = current_playback.position_ms as u64;
|
||||
t
|
||||
|
|
@ -179,10 +230,27 @@ pub async fn current_track(ctx: &Context, _req: &Request, res: &mut Response) ->
|
|||
}
|
||||
|
||||
let mut track = rb::playback::current_track();
|
||||
let path: Option<String> = track.as_ref().map(|t| t.path.clone());
|
||||
if let Some(path) = path {
|
||||
let hash = format!("{:x}", md5::compute(path.as_bytes()));
|
||||
if let Some(metadata) = repo::track::find_by_md5(ctx.pool.clone(), &hash).await? {
|
||||
let audio_path: Option<String> = track.as_ref().map(|t| t.path.clone());
|
||||
|
||||
// Use the playlist filename for DB lookup — it matches the key under which
|
||||
// metadata was saved (e.g. the HTTP URL) and is what the broker uses to
|
||||
// display album art correctly. audio_current_track()->path can diverge for
|
||||
// HTTP stream tracks so we treat it only as a fallback.
|
||||
let playlist_index = rb::playlist::index();
|
||||
let playlist_path = if playlist_index >= 0 {
|
||||
let info = rb::playlist::get_track_info(playlist_index);
|
||||
if !info.filename.is_empty() {
|
||||
Some(info.filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lookup_path = playlist_path.or(audio_path);
|
||||
if let Some(path) = lookup_path {
|
||||
if let Some(metadata) = find_track_metadata(ctx, &path).await? {
|
||||
track.as_mut().unwrap().id = Some(metadata.id);
|
||||
track.as_mut().unwrap().album_art = metadata.album_art;
|
||||
track.as_mut().unwrap().album_id = Some(metadata.album_id);
|
||||
|
|
@ -196,6 +264,83 @@ pub async fn current_track(ctx: &Context, _req: &Request, res: &mut Response) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_track_metadata(
|
||||
ctx: &Context,
|
||||
path: &str,
|
||||
) -> Result<Option<rockbox_library::entity::track::Track>, Error> {
|
||||
let hash = format!("{:x}", md5::compute(path.as_bytes()));
|
||||
let mut metadata = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
let internal_track = find_internal_track_by_url(ctx, path).await?;
|
||||
|
||||
if metadata
|
||||
.as_ref()
|
||||
.map(|track| track.album_art.is_none())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Some(track) = internal_track.clone() {
|
||||
metadata = Some(track);
|
||||
}
|
||||
}
|
||||
|
||||
if path.starts_with("http://") || path.starts_with("https://") {
|
||||
if metadata
|
||||
.as_ref()
|
||||
.map(|track| track.album_art.is_none())
|
||||
.unwrap_or(true)
|
||||
&& internal_track.is_none()
|
||||
{
|
||||
ensure_remote_track_metadata(path.to_string()).await?;
|
||||
metadata = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
async fn ensure_remote_track_metadata(path: String) -> Result<(), Error> {
|
||||
let status = tokio::task::spawn_blocking(move || -> Result<i32, Error> {
|
||||
let path_cstr = CString::new(path.as_str())?;
|
||||
Ok(unsafe { save_remote_track_metadata(path_cstr.as_ptr()) })
|
||||
})
|
||||
.await??;
|
||||
|
||||
if status != 0 {
|
||||
return Err(anyhow!("failed to save remote metadata"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_internal_track_by_url(
|
||||
ctx: &Context,
|
||||
path: &str,
|
||||
) -> Result<Option<rockbox_library::entity::track::Track>, Error> {
|
||||
let url = match reqwest::Url::parse(path) {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
let mut segments = match url.path_segments() {
|
||||
Some(segments) => segments,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let Some("tracks") = segments.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(track_id) = segments.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if segments.next().is_some() || track_id.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
repo::track::find(ctx.pool.clone(), track_id)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn next_track(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> {
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
let player = ctx.player.lock().unwrap();
|
||||
|
|
@ -205,8 +350,25 @@ pub async fn next_track(ctx: &Context, _req: &Request, res: &mut Response) -> Re
|
|||
}
|
||||
|
||||
let mut track = rb::playback::next_track();
|
||||
let path: Option<String> = track.as_ref().map(|t| t.path.clone());
|
||||
if let Some(path) = path {
|
||||
let audio_path: Option<String> = track.as_ref().map(|t| t.path.clone());
|
||||
|
||||
// Use the next playlist entry's filename for DB lookup for the same reason
|
||||
// as in current_track: playlist filename matches the saved metadata key.
|
||||
let current_index = rb::playlist::index();
|
||||
let next_index = current_index + 1;
|
||||
let playlist_path = if next_index >= 0 && next_index < rb::playlist::amount() {
|
||||
let info = rb::playlist::get_track_info(next_index);
|
||||
if !info.filename.is_empty() {
|
||||
Some(info.filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lookup_path = playlist_path.or(audio_path);
|
||||
if let Some(path) = lookup_path {
|
||||
let hash = format!("{:x}", md5::compute(path.as_bytes()));
|
||||
if let Some(metadata) = repo::track::find_by_md5(ctx.pool.clone(), &hash).await? {
|
||||
track.as_mut().unwrap().id = Some(metadata.id);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use std::env;
|
||||
use std::{env, ffi::CString};
|
||||
|
||||
use crate::http::{Context, Request, Response};
|
||||
use crate::PLAYER_MUTEX;
|
||||
use anyhow::Error;
|
||||
use anyhow::{anyhow, Error};
|
||||
use local_ip_addr::get_local_ip_address;
|
||||
use rand::seq::SliceRandom;
|
||||
use rockbox_graphql::read_files;
|
||||
use rockbox_library::repo;
|
||||
use rockbox_network::download_tracks;
|
||||
use rockbox_sys::{
|
||||
self as rb,
|
||||
types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo},
|
||||
|
|
@ -16,33 +15,47 @@ use rockbox_sys::{
|
|||
use rockbox_traits::types::track::Track;
|
||||
use rockbox_types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn save_remote_track_metadata(url: *const std::ffi::c_char) -> i32;
|
||||
}
|
||||
|
||||
pub async fn create_playlist(
|
||||
_ctx: &Context,
|
||||
req: &Request,
|
||||
res: &mut Response,
|
||||
) -> Result<(), Error> {
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
if req.body.is_none() {
|
||||
res.set_status(400);
|
||||
return Ok(());
|
||||
}
|
||||
let body = req.body.as_ref().unwrap();
|
||||
let mut new_playlist: NewPlaylist = serde_json::from_str(body).unwrap();
|
||||
let new_playlist: NewPlaylist = serde_json::from_str(body).unwrap();
|
||||
|
||||
if new_playlist.tracks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
new_playlist.tracks = download_tracks(new_playlist.tracks).await?;
|
||||
persist_remote_track_metadata(_ctx, &new_playlist.tracks).await?;
|
||||
|
||||
let dir = new_playlist.tracks[0].clone();
|
||||
let dir_parts: Vec<_> = dir.split('/').collect();
|
||||
let dir = dir_parts[0..dir_parts.len() - 1].join("/");
|
||||
let status = rb::playlist::create(&dir, None);
|
||||
if status == -1 {
|
||||
res.set_status(500);
|
||||
return Ok(());
|
||||
}
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
|
||||
// Always create a fresh playlist so the currently-playing track is
|
||||
// fully replaced rather than appended to.
|
||||
// Local paths: use the track's parent directory (required by Rockbox).
|
||||
// HTTP URLs: use the home directory — "/" fails because it isn't writable
|
||||
// and playlist_create needs to create its control file there.
|
||||
let first = &new_playlist.tracks[0];
|
||||
let dir = if first.starts_with("http://") || first.starts_with("https://") {
|
||||
std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string())
|
||||
} else {
|
||||
let parts: Vec<_> = first.split('/').collect();
|
||||
parts[..parts.len().saturating_sub(1)].join("/")
|
||||
};
|
||||
rb::playlist::create(&dir, None);
|
||||
|
||||
// URLs are passed as-is; codec detection happens in the C metadata layer
|
||||
// via probe_content_type_format(), which reads the HTTP Content-Type header
|
||||
// and overrides any extension-based guess.
|
||||
let start_index = rb::playlist::build_playlist(
|
||||
new_playlist.tracks.iter().map(|t| t.as_str()).collect(),
|
||||
0,
|
||||
|
|
@ -164,9 +177,21 @@ pub async fn get_playlist_tracks(
|
|||
}
|
||||
|
||||
pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> {
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
let req_body = req.body.as_ref().unwrap();
|
||||
let mut tracklist: InsertTracks = serde_json::from_str(&req_body).unwrap();
|
||||
|
||||
if let Some(dir) = &tracklist.directory {
|
||||
tracklist.tracks = read_files(dir.clone()).await?;
|
||||
}
|
||||
|
||||
if tracklist.tracks.is_empty() {
|
||||
res.text("0");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
persist_remote_track_metadata(ctx, &tracklist.tracks).await?;
|
||||
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
let amount = rb::playlist::amount();
|
||||
|
||||
let mut player = ctx.player.lock().unwrap();
|
||||
|
|
@ -215,19 +240,14 @@ pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) ->
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(dir) = &tracklist.directory {
|
||||
tracklist.tracks = read_files(dir.clone()).await?;
|
||||
}
|
||||
|
||||
if tracklist.tracks.is_empty() {
|
||||
res.text("0");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if amount == 0 {
|
||||
let dir = tracklist.tracks[0].clone();
|
||||
let dir_parts: Vec<_> = dir.split('/').collect();
|
||||
let dir = dir_parts[0..dir_parts.len() - 1].join("/");
|
||||
let first = &tracklist.tracks[0];
|
||||
let dir = if first.starts_with("http://") || first.starts_with("https://") {
|
||||
"/".to_string()
|
||||
} else {
|
||||
let dir_parts: Vec<_> = first.split('/').collect();
|
||||
dir_parts[0..dir_parts.len() - 1].join("/")
|
||||
};
|
||||
let status = rb::playlist::create(&dir, None);
|
||||
if status == -1 {
|
||||
res.set_status(500);
|
||||
|
|
@ -261,6 +281,91 @@ pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_remote_track_metadata(ctx: &Context, tracks: &[String]) -> Result<(), Error> {
|
||||
for track in tracks {
|
||||
if track.starts_with("http://") || track.starts_with("https://") {
|
||||
if find_internal_track_by_url(ctx, track).await?.is_some() {
|
||||
continue;
|
||||
}
|
||||
let track = track.clone();
|
||||
let track_for_worker = track.clone();
|
||||
let status = tokio::task::spawn_blocking(move || -> Result<i32, Error> {
|
||||
let track_cstr = CString::new(track_for_worker.as_str())?;
|
||||
Ok(unsafe { save_remote_track_metadata(track_cstr.as_ptr()) })
|
||||
})
|
||||
.await??;
|
||||
if status != 0 {
|
||||
return Err(anyhow!("failed to save remote metadata for {}", track));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_track_metadata(
|
||||
ctx: &Context,
|
||||
path: &str,
|
||||
) -> Result<Option<rockbox_library::entity::track::Track>, Error> {
|
||||
let hash = format!("{:x}", md5::compute(path.as_bytes()));
|
||||
let mut metadata = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
let internal_track = find_internal_track_by_url(ctx, path).await?;
|
||||
|
||||
if metadata
|
||||
.as_ref()
|
||||
.map(|track| track.album_art.is_none())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Some(track) = internal_track.clone() {
|
||||
metadata = Some(track);
|
||||
}
|
||||
}
|
||||
|
||||
if path.starts_with("http://") || path.starts_with("https://") {
|
||||
if metadata
|
||||
.as_ref()
|
||||
.map(|track| track.album_art.is_none())
|
||||
.unwrap_or(true)
|
||||
&& internal_track.is_none()
|
||||
{
|
||||
persist_remote_track_metadata(ctx, &[path.to_string()]).await?;
|
||||
metadata = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
async fn find_internal_track_by_url(
|
||||
ctx: &Context,
|
||||
path: &str,
|
||||
) -> Result<Option<rockbox_library::entity::track::Track>, Error> {
|
||||
let url = match reqwest::Url::parse(path) {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
let mut segments = match url.path_segments() {
|
||||
Some(segments) => segments,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let Some("tracks") = segments.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(track_id) = segments.next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if segments.next().is_some() || track_id.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
repo::track::find(ctx.pool.clone(), track_id)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn remove_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> {
|
||||
let player_mutex = PLAYER_MUTEX.lock().unwrap();
|
||||
let player = ctx.player.lock().unwrap();
|
||||
|
|
@ -309,7 +414,7 @@ pub async fn current_playlist(
|
|||
continue;
|
||||
}
|
||||
|
||||
let track = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
let track = find_track_metadata(ctx, &info.filename).await?;
|
||||
|
||||
if track.is_none() {
|
||||
entries.push(entry.clone());
|
||||
|
|
@ -344,10 +449,30 @@ pub async fn get_playlist(ctx: &Context, _req: &Request, res: &mut Response) ->
|
|||
false => 0,
|
||||
} as i32;
|
||||
|
||||
let mut entries = Vec::with_capacity(tracks.len());
|
||||
for (mut track, _) in tracks {
|
||||
if track.path.is_empty() {
|
||||
track.path = track.uri.clone();
|
||||
}
|
||||
|
||||
let mut entry: rockbox_sys::types::mp3_entry::Mp3Entry = track.into();
|
||||
if !entry.path.is_empty() {
|
||||
if let Some(metadata) = find_track_metadata(ctx, &entry.path).await? {
|
||||
entry.id = Some(metadata.id);
|
||||
entry.album_art = metadata.album_art.or(entry.album_art.clone());
|
||||
entry.album_id = Some(metadata.album_id);
|
||||
entry.artist_id = Some(metadata.artist_id);
|
||||
entry.genre_id = Some(metadata.genre_id);
|
||||
}
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
let result = PlaylistInfo {
|
||||
amount: tracks.len() as i32,
|
||||
amount: entries.len() as i32,
|
||||
index,
|
||||
entries: tracks.into_iter().map(|(t, _)| t.into()).collect(),
|
||||
entries,
|
||||
..Default::default()
|
||||
};
|
||||
res.json(&result);
|
||||
|
|
@ -369,7 +494,7 @@ pub async fn get_playlist(ctx: &Context, _req: &Request, res: &mut Response) ->
|
|||
continue;
|
||||
}
|
||||
|
||||
let track = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?;
|
||||
let track = find_track_metadata(ctx, &info.filename).await?;
|
||||
|
||||
if track.is_none() {
|
||||
entries.push(entry.clone());
|
||||
|
|
|
|||
|
|
@ -26,6 +26,25 @@ pub mod kv;
|
|||
pub mod player_events;
|
||||
pub mod scan;
|
||||
|
||||
// Force netstream FFI symbols into the staticlib output.
|
||||
// These functions are called from C code (streamfd.c) but not from any Rust
|
||||
// code, so rustc would otherwise drop the entire crate from librockbox_server.a.
|
||||
#[allow(dead_code)]
|
||||
mod _netstream {
|
||||
use rbnetstream::{rb_net_close, rb_net_len, rb_net_lseek, rb_net_open, rb_net_read};
|
||||
use std::ffi::{c_char, c_void};
|
||||
#[used]
|
||||
static FN_OPEN: unsafe extern "C" fn(*const c_char) -> i32 = rb_net_open;
|
||||
#[used]
|
||||
static FN_READ: unsafe extern "C" fn(i32, *mut c_void, usize) -> i64 = rb_net_read;
|
||||
#[used]
|
||||
static FN_LSEEK: extern "C" fn(i32, i64, i32) -> i64 = rb_net_lseek;
|
||||
#[used]
|
||||
static FN_LEN: extern "C" fn(i32) -> i64 = rb_net_len;
|
||||
#[used]
|
||||
static FN_CLOSE: extern "C" fn(i32) = rb_net_close;
|
||||
}
|
||||
|
||||
pub const AUDIO_EXTENSIONS: [&str; 17] = [
|
||||
"mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus",
|
||||
"spx", "sid", "ape", "wma",
|
||||
|
|
@ -281,13 +300,49 @@ pub extern "C" fn start_broker() {
|
|||
|
||||
match rb::playback::current_track() {
|
||||
Some(current_track) => {
|
||||
let hash = format!("{:x}", md5::compute(current_track.path.as_bytes()));
|
||||
if let Ok(Some(metadata)) =
|
||||
rt.block_on(repo::track::find_by_md5(pool.clone(), &hash))
|
||||
{
|
||||
let mut track: Track = current_track.into();
|
||||
// audio_current_track()->path can diverge from the playlist
|
||||
// filename for HTTP stream tracks. Use the playlist entry's
|
||||
// filename (same key the playlist section uses) so DB lookups
|
||||
// hit the right record and album_art is resolved correctly.
|
||||
let playlist_index = rb::playlist::index();
|
||||
let lookup_path = if playlist_index >= 0 {
|
||||
let info = rb::playlist::get_track_info(playlist_index);
|
||||
if !info.filename.is_empty() {
|
||||
info.filename
|
||||
} else {
|
||||
current_track.path.clone()
|
||||
}
|
||||
} else {
|
||||
current_track.path.clone()
|
||||
};
|
||||
|
||||
let hash = format!("{:x}", md5::compute(lookup_path.as_bytes()));
|
||||
let db_metadata = rt
|
||||
.block_on(repo::track::find_by_md5(pool.clone(), &hash))
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let mut track: Track = current_track.into();
|
||||
|
||||
if let Some(metadata) = db_metadata {
|
||||
// When the URL-keyed record has no album_art (it was saved
|
||||
// from the HTTP stream which has no embedded art), fall back
|
||||
// to the local track identified by the UUID in the URL path.
|
||||
let album_art = if metadata.album_art.is_some() {
|
||||
metadata.album_art.clone()
|
||||
} else {
|
||||
let uuid = lookup_path.rsplit('/').next().unwrap_or("");
|
||||
if !uuid.is_empty() {
|
||||
rt.block_on(repo::track::find(pool.clone(), uuid))
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|t| t.album_art)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
track.id = Some(metadata.id.clone());
|
||||
track.album_art = metadata.album_art;
|
||||
track.album_art = album_art;
|
||||
track.album_id = Some(metadata.album_id);
|
||||
track.artist_id = Some(metadata.artist_id);
|
||||
SimpleBroker::publish(track.clone());
|
||||
|
|
@ -333,6 +388,10 @@ pub extern "C" fn start_broker() {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Track not in DB (e.g. an HTTP stream URL not yet scanned).
|
||||
// Still publish so clients receive elapsed/status updates.
|
||||
SimpleBroker::publish(track);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
|
@ -340,42 +399,85 @@ pub extern "C" fn start_broker() {
|
|||
}
|
||||
};
|
||||
|
||||
let mut entries: Vec<Mp3Entry> = vec![];
|
||||
|
||||
let mut current_playlist = rb::playlist::get_current();
|
||||
let amount = rb::playlist::amount();
|
||||
|
||||
// Collect track info while holding the mutex (quick — no I/O).
|
||||
let mut track_infos: Vec<(String, String)> = Vec::with_capacity(amount as usize);
|
||||
for i in 0..amount {
|
||||
let info = rb::playlist::get_track_info(i);
|
||||
let mut entry = rb::metadata::get_metadata(-1, &info.filename);
|
||||
|
||||
let hash = format!("{:x}", md5::compute(info.filename.as_bytes()));
|
||||
track_infos.push((hash, info.filename));
|
||||
}
|
||||
|
||||
if let Some(entry) = metadata_cache.get(&hash) {
|
||||
// Release the mutex before any slow network I/O so that player
|
||||
// commands (play, pause, …) are not blocked while metadata is fetched.
|
||||
drop(player_mutex);
|
||||
|
||||
// Probe metadata for uncached tracks without holding player_mutex.
|
||||
let mut entries: Vec<Mp3Entry> = vec![];
|
||||
for (hash, filename) in &track_infos {
|
||||
if let Some(entry) = metadata_cache.get(hash) {
|
||||
entries.push(entry.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let track = rt
|
||||
.block_on(repo::track::find_by_md5(pool.clone(), &hash))
|
||||
let is_http = filename.starts_with("http://") || filename.starts_with("https://");
|
||||
let db_track = rt
|
||||
.block_on(repo::track::find_by_md5(pool.clone(), hash))
|
||||
.unwrap();
|
||||
|
||||
if track.is_none() {
|
||||
entries.push(entry);
|
||||
continue;
|
||||
}
|
||||
let mut entry = if is_http {
|
||||
// For HTTP stream URLs, do NOT call get_metadata(-1, url) — that
|
||||
// opens a live HTTP connection for every queued track and blocks
|
||||
// the broker loop. Instead, build the entry from the database
|
||||
// using the saved track metadata keyed by the URL hash.
|
||||
let mut e = Mp3Entry::default();
|
||||
e.path = filename.clone();
|
||||
if let Some(ref t) = db_track {
|
||||
e.title = t.title.clone();
|
||||
e.artist = t.artist.clone();
|
||||
e.album = t.album.clone();
|
||||
e.albumartist = t.album_artist.clone();
|
||||
e.length = t.length as u64;
|
||||
e.bitrate = t.bitrate;
|
||||
e.frequency = t.frequency as u64;
|
||||
// URL-keyed record may have no album_art (stream has no
|
||||
// embedded art). Fall back to the local track by UUID.
|
||||
e.album_art = if t.album_art.is_some() {
|
||||
t.album_art.clone()
|
||||
} else {
|
||||
let uuid = filename.rsplit('/').next().unwrap_or("");
|
||||
if !uuid.is_empty() {
|
||||
rt.block_on(repo::track::find(pool.clone(), uuid))
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|local| local.album_art)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
e.album_id = Some(t.album_id.clone());
|
||||
e.artist_id = Some(t.artist_id.clone());
|
||||
e.genre_id = Some(t.genre_id.clone());
|
||||
e.id = Some(t.id.clone());
|
||||
}
|
||||
e
|
||||
} else {
|
||||
let mut e = rb::metadata::get_metadata(-1, filename);
|
||||
if db_track.is_some() {
|
||||
e.album_art = db_track.as_ref().map(|t| t.album_art.clone()).flatten();
|
||||
e.album_id = db_track.as_ref().map(|t| t.album_id.clone());
|
||||
e.artist_id = db_track.as_ref().map(|t| t.artist_id.clone());
|
||||
e.genre_id = db_track.as_ref().map(|t| t.genre_id.clone());
|
||||
}
|
||||
e
|
||||
};
|
||||
|
||||
entry.album_art = track.as_ref().map(|t| t.album_art.clone()).flatten();
|
||||
entry.album_id = track.as_ref().map(|t| t.album_id.clone());
|
||||
entry.artist_id = track.as_ref().map(|t| t.artist_id.clone());
|
||||
entry.genre_id = track.as_ref().map(|t| t.genre_id.clone());
|
||||
|
||||
metadata_cache.insert(hash, entry.clone());
|
||||
metadata_cache.insert(hash.clone(), entry.clone());
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
drop(player_mutex);
|
||||
|
||||
current_playlist.amount = amount;
|
||||
current_playlist.max_playlist_size = rb::playlist::max_playlist_size();
|
||||
current_playlist.index = rb::playlist::index();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
directory, create subdirectories and do all sorts of things you want to be
|
||||
able to browse when you fire up the simulator.
|
||||
|
||||
Rust toolchain (edition 2021) and Cargo must be installed to build the
|
||||
network-stream support library (crates/netstream). The build will run
|
||||
`cargo build --manifest-path crates/netstream/Cargo.toml --release`
|
||||
automatically during `make`.
|
||||
|
||||
3. Run Uisimulator
|
||||
|
||||
To create a simulated disk drive for the simulator to see, create a
|
||||
|
|
@ -46,14 +51,45 @@
|
|||
|
||||
Run 'rockboxui'.
|
||||
|
||||
4. Target Keypad Equivalents
|
||||
4. Network Stream Playback (HTTP/HTTPS URLs)
|
||||
|
||||
The SDL simulator supports playing audio directly from HTTP and HTTPS URLs.
|
||||
URLs are treated exactly like local file paths everywhere in the UI (file
|
||||
browser, playlists, etc.).
|
||||
|
||||
To play a URL:
|
||||
|
||||
a) Playlist method (recommended):
|
||||
Create a plain-text .m3u or .m3u8 playlist file inside your simdisk,
|
||||
with one URL per line, e.g.:
|
||||
|
||||
simdisk/streams.m3u
|
||||
----------------------
|
||||
https://example.com/track1.mp3
|
||||
https://example.com/track2.ogg
|
||||
|
||||
Load the playlist from the file browser and press Play.
|
||||
|
||||
b) Direct entry (if the target UI supports text input for file paths):
|
||||
Enter the full URL including scheme, e.g.:
|
||||
https://example.com/audio/song.mp3
|
||||
|
||||
Requirements:
|
||||
* The server must serve the file with a correct Content-Type header for
|
||||
the codec to be selected automatically.
|
||||
* Seeking works for servers that support HTTP Range requests (RFC 7233).
|
||||
For servers that do not support Range requests, seeking will fail
|
||||
gracefully and playback continues from the current position.
|
||||
* HTTPS is supported via rustls (no system CA store dependency).
|
||||
|
||||
5. Target Keypad Equivalents
|
||||
|
||||
The keyboard's numerical keypad is used to simulate the target keypad. See
|
||||
the output rockboxui displays on start for details.
|
||||
|
||||
5. Mouse Input
|
||||
6. Mouse Input
|
||||
|
||||
Clicking on the button images on the background will simulate pressing the
|
||||
appropriate buttons. On scroll wheel targts the mouse wheel will simulate
|
||||
appropriate buttons. On scroll wheel targets the mouse wheel will simulate
|
||||
scroll wheel motion.
|
||||
|
||||
|
|
|
|||
|
|
@ -678,6 +678,7 @@ static bool read_chunk_minf(qtmovie_t *qtmovie, size_t chunk_len)
|
|||
static bool read_chunk_mdia(qtmovie_t *qtmovie, size_t chunk_len)
|
||||
{
|
||||
size_t size_remaining = chunk_len - 8;
|
||||
fourcc_t handler = 0;
|
||||
|
||||
while (size_remaining)
|
||||
{
|
||||
|
|
@ -696,9 +697,23 @@ static bool read_chunk_mdia(qtmovie_t *qtmovie, size_t chunk_len)
|
|||
|
||||
switch (sub_chunk_id)
|
||||
{
|
||||
case MAKEFOURCC('h','d','l','r'):
|
||||
/* version/flags + pre_defined */
|
||||
stream_skip(qtmovie->stream, 8);
|
||||
handler = stream_read_uint32(qtmovie->stream);
|
||||
if (sub_chunk_len > 20)
|
||||
stream_skip(qtmovie->stream, sub_chunk_len - 20);
|
||||
break;
|
||||
case MAKEFOURCC('m','i','n','f'):
|
||||
if (!read_chunk_minf(qtmovie, sub_chunk_len)) {
|
||||
return false;
|
||||
if (handler == MAKEFOURCC('s','o','u','n'))
|
||||
{
|
||||
if (!read_chunk_minf(qtmovie, sub_chunk_len)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
stream_skip(qtmovie->stream, sub_chunk_len - 8);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -867,4 +882,3 @@ int qtmovie_read(stream_t *file, demux_res_t *demux_res)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include <stdio.h>
|
||||
#include "metadata.h"
|
||||
#include "metadata_common.h"
|
||||
#include "logf.h"
|
||||
#include "metadata_parsers.h"
|
||||
#include "platform.h"
|
||||
|
|
@ -71,7 +72,7 @@ bool get_a52_metadata(int fd, struct mp3entry *id3)
|
|||
|
||||
id3->bitrate = a52_bitrates[i >> 1];
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
switch (buf[4] & 0xc0)
|
||||
{
|
||||
|
|
@ -97,7 +98,7 @@ bool get_a52_metadata(int fd, struct mp3entry *id3)
|
|||
}
|
||||
|
||||
/* One A52 frame contains 6 blocks, each containing 256 samples */
|
||||
totalsamples = id3->filesize / id3->bytesperframe * 6 * 256;
|
||||
totalsamples = id3->FS_PREFIX(filesize) / id3->bytesperframe * 6 * 256;
|
||||
id3->length = totalsamples / id3->frequency * 1000;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ bool get_aac_metadata(int fd, struct mp3entry *entry)
|
|||
entry->id3v1len = 0;
|
||||
entry->id3v2len = getid3v2len(fd);
|
||||
entry->first_frame_offset = entry->id3v2len;
|
||||
entry->filesize = filesize(fd) - entry->first_frame_offset;
|
||||
entry->FS_PREFIX(filesize) = filesize(fd) - entry->first_frame_offset;
|
||||
entry->needs_upsampling_correction = false;
|
||||
|
||||
if (entry->id3v2len)
|
||||
|
|
@ -138,7 +138,7 @@ bool get_aac_metadata(int fd, struct mp3entry *entry)
|
|||
return get_mp4_metadata(fd, entry);
|
||||
}
|
||||
|
||||
entry->length = (unsigned long)((entry->filesize * 8LL + (entry->bitrate >> 1)) / entry->bitrate);
|
||||
entry->length = (unsigned long)((entry->FS_PREFIX(filesize) * 8LL + (entry->bitrate >> 1)) / entry->bitrate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ bool get_adx_metadata(int fd, struct mp3entry* id3)
|
|||
id3->bitrate = id3->frequency * channels * 18 * 8 / 32 / 1000;
|
||||
id3->length = get_long_be(&buf[12]) / id3->frequency * 1000;
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
/* get loop info */
|
||||
if (!memcmp(buf+0x10,"\x01\xF4\x03",3)) {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ bool get_aiff_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->vbr = false; /* AIFF files are CBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ static bool parse_sap_header(int fd, struct mp3entry* id3, int file_len)
|
|||
bool get_asap_metadata(int fd, struct mp3entry* id3)
|
||||
{
|
||||
|
||||
int filelength = filesize(fd);
|
||||
int filelength = metadata_filesize(fd);
|
||||
|
||||
if(parse_sap_header(fd, id3, filelength) == false)
|
||||
{
|
||||
|
|
@ -254,7 +254,7 @@ bool get_asap_metadata(int fd, struct mp3entry* id3)
|
|||
id3->frequency = 44100;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filelength;
|
||||
id3->FS_PREFIX(filesize) = filelength;
|
||||
id3->genre_string = id3_get_num_genre(36);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -642,7 +642,7 @@ bool get_asf_metadata(int fd, struct mp3entry* id3)
|
|||
header.
|
||||
*/
|
||||
id3->first_frame_offset = lseek(fd, 0, SEEK_CUR) + 26;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* We copy the wfx struct to the MP3 TOC field in the id3 struct so
|
||||
the codec doesn't need to parse the header object again */
|
||||
memcpy(id3->toc, &wfx, sizeof(wfx));
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ bool get_au_metadata(int fd, struct mp3entry* id3)
|
|||
int offset;
|
||||
|
||||
id3->vbr = false; /* All Sun audio files are CBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->length = 0;
|
||||
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
|
|
@ -72,7 +72,7 @@ bool get_au_metadata(int fd, struct mp3entry* id3)
|
|||
* bits per sample: 8 bit
|
||||
* channel: mono
|
||||
*/
|
||||
numbytes = id3->filesize;
|
||||
numbytes = id3->FS_PREFIX(filesize);
|
||||
id3->frequency = 8000;
|
||||
id3->bitrate = 8;
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ bool get_au_metadata(int fd, struct mp3entry* id3)
|
|||
/* data size */
|
||||
numbytes = get_long_be(buf + 8);
|
||||
if (numbytes == (uint32_t)0xffffffff)
|
||||
numbytes = id3->filesize - offset;
|
||||
numbytes = id3->FS_PREFIX(filesize) - offset;
|
||||
|
||||
id3->frequency = get_long_be(buf + 16);
|
||||
id3->bitrate = get_au_bitspersample(get_long_be(buf + 12)) * get_long_be(buf + 20)
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ bool get_ay_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ bool get_flac_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->vbr = true; /* All FLAC files are VBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->frequency = (buf[10] << 12) | (buf[11] << 4)
|
||||
| ((buf[12] & 0xf0) >> 4);
|
||||
rc = true; /* Got vital metadata */
|
||||
|
|
@ -134,7 +134,7 @@ bool get_flac_metadata(int fd, struct mp3entry* id3)
|
|||
{
|
||||
/* Calculate track length (in ms) and estimate the bitrate (in kbit/s) */
|
||||
id3->length = ((int64_t) totalsamples * 1000) / id3->frequency;
|
||||
id3->bitrate = (id3->filesize * 8) / id3->length;
|
||||
id3->bitrate = (id3->FS_PREFIX(filesize) * 8) / id3->length;
|
||||
}
|
||||
else if (totalsamples == 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ bool get_gbs_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* we only render 16 bits, 44.1KHz, Stereo */
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ bool get_hes_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* we only render 16 bits, 44.1KHz, Stereo */
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ bool get_kss_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* we only render 16 bits, 44.1KHz, Stereo */
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -34,11 +34,17 @@
|
|||
|
||||
#include "metadata/metadata_common.h"
|
||||
|
||||
/* Unified stream I/O: routes HTTP(S) fds through the Rust netstream layer.
|
||||
* Only available in simulator / hosted SDL application builds. */
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
#include "streamfd.h"
|
||||
#endif
|
||||
|
||||
static bool get_shn_metadata(int fd, struct mp3entry *id3)
|
||||
{
|
||||
/* TODO: read the id3v2 header if it exists */
|
||||
id3->vbr = true;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
return skip_id3v2(fd, id3);
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +53,7 @@ static bool get_other_asap_metadata(int fd, struct mp3entry *id3)
|
|||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->genre_string = id3_get_num_genre(36);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -414,6 +420,53 @@ unsigned int probe_file_format(const char *filename)
|
|||
return AFMT_UNKNOWN;
|
||||
}
|
||||
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
static unsigned int probe_content_type_format(int fd)
|
||||
{
|
||||
char content_type[64];
|
||||
if (stream_content_type(fd, content_type, sizeof(content_type)) < 0)
|
||||
return AFMT_UNKNOWN;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/mpeg") ||
|
||||
!strcasecmp(content_type, "audio/mp3") ||
|
||||
!strcasecmp(content_type, "audio/mpa"))
|
||||
return AFMT_MPA_L3;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/mp4") ||
|
||||
!strcasecmp(content_type, "audio/m4a") ||
|
||||
!strcasecmp(content_type, "audio/x-m4a") ||
|
||||
!strcasecmp(content_type, "video/mp4") ||
|
||||
!strcasecmp(content_type, "application/mp4"))
|
||||
return AFMT_MP4_AAC;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/aac") ||
|
||||
!strcasecmp(content_type, "audio/aacp"))
|
||||
return AFMT_AAC_BSF;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/ogg") ||
|
||||
!strcasecmp(content_type, "application/ogg"))
|
||||
return AFMT_OGG_VORBIS;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/opus"))
|
||||
return AFMT_OPUS;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/flac") ||
|
||||
!strcasecmp(content_type, "audio/x-flac"))
|
||||
return AFMT_FLAC;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/wav") ||
|
||||
!strcasecmp(content_type, "audio/wave") ||
|
||||
!strcasecmp(content_type, "audio/x-wav"))
|
||||
return AFMT_PCM_WAV;
|
||||
|
||||
if (!strcasecmp(content_type, "audio/x-ms-wma") ||
|
||||
!strcasecmp(content_type, "audio/asf"))
|
||||
return AFMT_WMA;
|
||||
|
||||
return AFMT_UNKNOWN;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Get metadata for track - return false if parsing showed problems with the
|
||||
* file that would prevent playback. supply a filedescriptor <0 and the file will be opened
|
||||
* and closed automatically within the get_metadata call
|
||||
|
|
@ -427,16 +480,25 @@ bool get_metadata_ex(struct mp3entry* id3, int fd, const char* trackname, int fl
|
|||
const struct afmt_entry *entry;
|
||||
int logfd = 0;
|
||||
DEBUGF("Read metadata for %s\n", trackname);
|
||||
fprintf(stderr, "[metadata] get_metadata_ex: trackname=%s fd=%d\n", trackname, fd);
|
||||
const char *res_str = "\n";
|
||||
|
||||
/* Clear the mp3entry to avoid having bogus pointers appear */
|
||||
wipe_mp3entry(id3);
|
||||
|
||||
if (fd < 0)
|
||||
/* fd == -1 is the "not yet opened" sentinel. In simulator / APPLICATION
|
||||
* builds HTTP stream handles are encoded as fd <= -1000 and must not be
|
||||
* treated as "no fd provided". */
|
||||
if (fd == -1)
|
||||
{
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
fd = stream_open(trackname, O_RDONLY);
|
||||
#else
|
||||
fd = open(trackname, O_RDONLY);
|
||||
#endif
|
||||
flags |= METADATA_CLOSE_FD_ON_EXIT;
|
||||
if (fd < 0)
|
||||
fprintf(stderr, "[metadata] get_metadata_ex: opened fd=%d for %s\n", fd, trackname);
|
||||
if (fd == -1)
|
||||
{
|
||||
DEBUGF("Error opening %s\n", trackname);
|
||||
res_str = " - [Error opening]\n";
|
||||
|
|
@ -447,6 +509,20 @@ bool get_metadata_ex(struct mp3entry* id3, int fd, const char* trackname, int fl
|
|||
|
||||
/* Take our best guess at the codec type based on file extension */
|
||||
id3->codectype = probe_file_format(trackname);
|
||||
fprintf(stderr, "[metadata] probe_file_format: %s -> codectype=%u\n", trackname, id3->codectype);
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
/* For HTTP streams the Content-Type header is more reliable than the
|
||||
* file extension (e.g. .m4a is used for both ALAC and AAC). Let the
|
||||
* server's content-type override the extension-based guess whenever it
|
||||
* resolves to a known codec. */
|
||||
if (stream_is_http_fd(fd))
|
||||
{
|
||||
int ct_codec = probe_content_type_format(fd);
|
||||
fprintf(stderr, "[metadata] probe_content_type_format: fd=%d -> codectype=%u\n", fd, ct_codec);
|
||||
if (ct_codec != AFMT_UNKNOWN)
|
||||
id3->codectype = ct_codec;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* default values for embedded cuesheets */
|
||||
id3->has_embedded_cuesheet = false;
|
||||
|
|
@ -455,24 +531,41 @@ bool get_metadata_ex(struct mp3entry* id3, int fd, const char* trackname, int fl
|
|||
entry = &audio_formats[id3->codectype];
|
||||
|
||||
/* Load codec specific track tag information and confirm the codec type. */
|
||||
fprintf(stderr, "[metadata] parsing: trackname=%s codec=%u parser=%s\n",
|
||||
trackname, id3->codectype, entry->label ? entry->label : "none");
|
||||
if (!entry->parse_func)
|
||||
{
|
||||
DEBUGF("nothing to parse for %s (format %s)\n", trackname, entry->label);
|
||||
fprintf(stderr, "[metadata] ERROR: no parser for %s (codec=%u)\n", trackname, id3->codectype);
|
||||
res_str = " - [No parser]\n";
|
||||
success = false;
|
||||
}
|
||||
else if (!entry->parse_func(fd, id3))
|
||||
{
|
||||
DEBUGF("parsing %s failed (format: %s)\n", trackname, entry->label);
|
||||
fprintf(stderr, "[metadata] ERROR: parse failed for %s (format %s)\n", trackname, entry->label);
|
||||
res_str = " - [Parser failed]\n";
|
||||
success = false;
|
||||
wipe_mp3entry(id3); /* ensure the mp3entry is clear */
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "[metadata] parsed OK: %s length=%lu elapsed=%lu freq=%lu filesize=%lu codectype=%u first_frame_off=%lu\n",
|
||||
trackname, id3->length, id3->elapsed, id3->frequency,
|
||||
id3->FS_PREFIX(filesize), id3->codectype, id3->first_frame_offset);
|
||||
}
|
||||
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
if ((flags & METADATA_CLOSE_FD_ON_EXIT))
|
||||
stream_close(fd);
|
||||
else
|
||||
stream_lseek(fd, 0, SEEK_SET);
|
||||
#else
|
||||
if ((flags & METADATA_CLOSE_FD_ON_EXIT))
|
||||
close(fd);
|
||||
else
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
#endif
|
||||
|
||||
if (success && (flags & METADATA_EXCLUDE_ID3_PATH) == 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,6 +21,43 @@
|
|||
#include <inttypes.h>
|
||||
#include "metadata.h"
|
||||
|
||||
/* In simulator / hosted-SDL-app builds, file descriptors for HTTP(S) streams
|
||||
* are encoded as integers <= STREAM_HTTP_FD_BASE (-1000). Plain POSIX
|
||||
* read() / lseek() / filesize() do not handle those values. Route all
|
||||
* metadata I/O through the stream abstraction so that HTTP handles work
|
||||
* identically to normal file descriptors.
|
||||
*
|
||||
* streamfd.h is in apps/, which is on the include path via -I$(APPSDIR) in
|
||||
* uisimulator.make. The macros below are intentionally placed before the
|
||||
* read_uint* macros so those expansions are also redirected. */
|
||||
#if defined(SIMULATOR) || defined(APPLICATION)
|
||||
#include "streamfd.h"
|
||||
/* Route read() and lseek() through the stream abstraction so that HTTP(S)
|
||||
* file descriptors (encoded as integers <= STREAM_HTTP_FD_BASE = -1000) are
|
||||
* handled correctly by all metadata parsers.
|
||||
*
|
||||
* filesize() is intentionally NOT redefined here. It is also a member name
|
||||
* of struct mp3entry (expanded via FS_PREFIX in firmware/include/file.h), so
|
||||
* redefining it as a function-like macro would break every `id3->filesize`
|
||||
* struct access with a "no member named 'filesize'" compiler error.
|
||||
*
|
||||
* Instead, metadata parsers that need a correct file size (e.g. for CBR
|
||||
* duration estimation) must call metadata_filesize(fd), defined below.
|
||||
* stream_filesize_fd() returns Content-Length for HTTP fds and delegates to
|
||||
* the platform filesize() for regular fds. */
|
||||
#ifdef read
|
||||
#undef read
|
||||
#endif
|
||||
#define read(fd, buf, n) stream_read((fd), (buf), (n))
|
||||
#ifdef lseek
|
||||
#undef lseek
|
||||
#endif
|
||||
#define lseek(fd, off, whence) stream_lseek((fd), (off), (whence))
|
||||
#define metadata_filesize(fd) stream_filesize_fd((fd))
|
||||
#else
|
||||
#define metadata_filesize(fd) filesize(fd)
|
||||
#endif /* SIMULATOR || APPLICATION */
|
||||
|
||||
#ifdef ROCKBOX_BIG_ENDIAN
|
||||
#define IS_BIG_ENDIAN 1
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ bool get_mod_metadata(int fd, struct mp3entry* id3)
|
|||
id3->frequency = 44100;
|
||||
id3->length = 120*1000;
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,14 +83,14 @@ bool get_monkeys_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->vbr = true; /* All APE files are VBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
totalsamples = finalframeblocks;
|
||||
if (totalframes > 1)
|
||||
totalsamples += blocksperframe * (totalframes-1);
|
||||
|
||||
id3->length = ((int64_t) totalsamples * 1000) / id3->frequency;
|
||||
id3->bitrate = (id3->filesize * 8) / id3->length;
|
||||
id3->bitrate = (id3->FS_PREFIX(filesize) * 8) / id3->length;
|
||||
|
||||
read_ape_tags(fd, id3);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -73,14 +73,14 @@ static int getsonglength(int fd, struct mp3entry *entry)
|
|||
|
||||
/* Subtract the meta information from the file size to get
|
||||
the true size of the MP3 stream */
|
||||
entry->filesize -= entry->id3v1len + entry->id3v2len;
|
||||
entry->FS_PREFIX(filesize) -= entry->id3v1len + entry->id3v2len;
|
||||
|
||||
/* Validate byte count, in case the file has been edited without
|
||||
* updating the header.
|
||||
*/
|
||||
if (info.byte_count)
|
||||
{
|
||||
const unsigned long expected = entry->filesize - entry->id3v1len
|
||||
const unsigned long expected = entry->FS_PREFIX(filesize) - entry->id3v1len
|
||||
- entry->id3v2len;
|
||||
const unsigned long diff = MAX(10240, info.byte_count / 20);
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ static int getsonglength(int fd, struct mp3entry *entry)
|
|||
}
|
||||
}
|
||||
|
||||
entry->filesize -= bytecount;
|
||||
entry->FS_PREFIX(filesize) -= bytecount;
|
||||
bytecount += entry->id3v2len;
|
||||
|
||||
entry->bitrate = info.bitrate;
|
||||
|
|
@ -130,7 +130,7 @@ static int getsonglength(int fd, struct mp3entry *entry)
|
|||
if (info.bitrate < 8)
|
||||
filetime = 0;
|
||||
else
|
||||
filetime = entry->filesize / (info.bitrate >> 3);
|
||||
filetime = entry->FS_PREFIX(filesize) / (info.bitrate >> 3);
|
||||
/* bitrate is in kbps so this delivers milliseconds. Doing bitrate / 8
|
||||
* instead of filesize * 8 is exact, because mpeg audio bitrates are
|
||||
* always multiples of 8, and it avoids overflows. */
|
||||
|
|
@ -163,7 +163,7 @@ static int getsonglength(int fd, struct mp3entry *entry)
|
|||
bool get_mp3_metadata(int fd, struct mp3entry *entry)
|
||||
{
|
||||
entry->title = NULL;
|
||||
entry->filesize = filesize(fd);
|
||||
entry->FS_PREFIX(filesize) = metadata_filesize(fd);
|
||||
entry->id3v1len = getid3v1len(fd);
|
||||
entry->id3v2len = getid3v2len(fd);
|
||||
entry->tracknum = 0;
|
||||
|
|
@ -181,7 +181,7 @@ bool get_mp3_metadata(int fd, struct mp3entry *entry)
|
|||
setid3v1title(fd, entry);
|
||||
}
|
||||
|
||||
if(!entry->length || (entry->filesize < 8 ))
|
||||
if(!entry->length || (entry->FS_PREFIX(filesize) < 8 ))
|
||||
/* no song length or less than 8 bytes is hereby considered to be an
|
||||
invalid mp3 and won't be played by us! */
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#include "platform.h"
|
||||
|
||||
#include "metadata.h"
|
||||
#include "metadata_common.h"
|
||||
#include "metadata/metadata_parsers.h"
|
||||
|
||||
//#define DEBUG_VERBOSE
|
||||
|
|
@ -609,7 +610,7 @@ int get_mp3file_info(int fd, struct mp3info *info)
|
|||
if(result)
|
||||
return result;
|
||||
|
||||
info->byte_count = filesize(fd) - getid3v1len(fd) - offset - bytecount;
|
||||
info->byte_count = metadata_filesize(fd) - getid3v1len(fd) - offset - bytecount;
|
||||
}
|
||||
|
||||
return bytecount;
|
||||
|
|
|
|||
|
|
@ -777,7 +777,7 @@ static bool read_mp4_container(int fd, struct mp3entry* id3,
|
|||
if(size == 0)
|
||||
break;
|
||||
/* mdat chunks accumulate! */
|
||||
id3->filesize += size;
|
||||
id3->FS_PREFIX(filesize) += size;
|
||||
if(id3->samples > 0) {
|
||||
/* We've already seen the moov chunk. */
|
||||
done = true;
|
||||
|
|
@ -821,12 +821,12 @@ static bool read_mp4_container(int fd, struct mp3entry* id3,
|
|||
bool get_mp4_metadata(int fd, struct mp3entry* id3)
|
||||
{
|
||||
id3->codectype = AFMT_UNKNOWN;
|
||||
id3->filesize = 0;
|
||||
id3->FS_PREFIX(filesize) = 0;
|
||||
errno = 0;
|
||||
|
||||
if (read_mp4_container(fd, id3, filesize(fd)) && (errno == 0)
|
||||
if (read_mp4_container(fd, id3, metadata_filesize(fd)) && (errno == 0)
|
||||
&& (id3->samples > 0) && (id3->frequency > 0)
|
||||
&& (id3->filesize > 0))
|
||||
&& (id3->FS_PREFIX(filesize) > 0))
|
||||
{
|
||||
if (id3->codectype == AFMT_UNKNOWN)
|
||||
{
|
||||
|
|
@ -844,7 +844,7 @@ bool get_mp4_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
}
|
||||
|
||||
id3->bitrate = ((int64_t) id3->filesize * 8) / id3->length;
|
||||
id3->bitrate = ((int64_t) id3->FS_PREFIX(filesize) * 8) / id3->length;
|
||||
DEBUGF("MP4 bitrate %d, frequency %ld Hz, length %ld ms\n",
|
||||
id3->bitrate, id3->frequency, id3->length);
|
||||
}
|
||||
|
|
@ -853,7 +853,7 @@ bool get_mp4_metadata(int fd, struct mp3entry* id3)
|
|||
logf("MP4 metadata error");
|
||||
DEBUGF("MP4 metadata error. errno %d, samples %ld, frequency %ld, "
|
||||
"filesize %ld\n", errno, id3->samples, id3->frequency,
|
||||
id3->filesize);
|
||||
id3->FS_PREFIX(filesize));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,8 +212,8 @@ bool get_musepack_metadata(int fd, struct mp3entry *id3)
|
|||
return false;
|
||||
}
|
||||
|
||||
id3->filesize = filesize(fd);
|
||||
id3->bitrate = id3->filesize * 8 / id3->length;
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->bitrate = id3->FS_PREFIX(filesize) * 8 / id3->length;
|
||||
|
||||
read_ape_tags(fd, id3);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ bool get_nsf_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* we only render 16 bits, 44.1KHz, Mono */
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ bool get_ogg_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
}
|
||||
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
/* We need to ensure the serial number from this page is the same as the
|
||||
* one from the last page (since we only support a single bitstream).
|
||||
|
|
@ -148,7 +148,7 @@ bool get_ogg_metadata(int fd, struct mp3entry* id3)
|
|||
*/
|
||||
|
||||
/* A page is always < 64 kB */
|
||||
if (lseek(fd, -(MIN(64 * 1024, id3->filesize)), SEEK_END) < 0)
|
||||
if (lseek(fd, -(MIN(64 * 1024, id3->FS_PREFIX(filesize))), SEEK_END) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ bool get_ogg_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
}
|
||||
|
||||
id3->bitrate = (((int64_t) id3->filesize - comment_size) * 8) / id3->length;
|
||||
id3->bitrate = (((int64_t) id3->FS_PREFIX(filesize) - comment_size) * 8) / id3->length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
#include <string.h>
|
||||
#include "platform.h"
|
||||
#include "metadata.h"
|
||||
#include "metadata_common.h"
|
||||
#include "metadata_parsers.h"
|
||||
|
||||
#define EA3_HEADER_SIZE 96
|
||||
|
|
@ -179,7 +180,7 @@ bool get_oma_metadata(int fd, struct mp3entry* id3)
|
|||
|
||||
/* Currently, there's no means of knowing the duration *
|
||||
* directly from the the file so we calculate it. */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->length = ((id3->filesize - id3->first_frame_offset) * 8) / id3->bitrate;
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->length = ((id3->FS_PREFIX(filesize) - id3->first_frame_offset) * 8) / id3->bitrate;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ static inline int rm_parse_header(int fd, RMContext *rmctx, struct mp3entry *id3
|
|||
rmctx->data_offset = skipped + 8;
|
||||
rmctx->bit_rate = rmctx->block_align * rmctx->sample_rate / 192;
|
||||
if (rmctx->block_align)
|
||||
rmctx->nb_packets = (filesize(fd) - rmctx->data_offset) / rmctx->block_align;
|
||||
rmctx->nb_packets = (metadata_filesize(fd) - rmctx->data_offset) / rmctx->block_align;
|
||||
if (rmctx->sample_rate)
|
||||
rmctx->duration = (uint32_t)(256LL * 6 * 1000 * rmctx->nb_packets / rmctx->sample_rate);
|
||||
rmctx->flags |= RM_RAW_DATASTREAM;
|
||||
|
|
@ -505,6 +505,6 @@ bool get_rm_metadata(int fd, struct mp3entry* id3)
|
|||
id3->bitrate = (rmctx->bit_rate + 500) / 1000;
|
||||
id3->frequency = rmctx->sample_rate;
|
||||
id3->length = rmctx->duration;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = metadata_filesize(fd);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ bool get_sgc_metadata(int fd, struct mp3entry* id3)
|
|||
return false;
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
/* we only render 16 bits, 44.1KHz, Stereo */
|
||||
id3->bitrate = 706;
|
||||
id3->frequency = 44100;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ bool get_sid_metadata(int fd, struct mp3entry* id3)
|
|||
*/
|
||||
id3->length = (buf[0xf]-1)*1000;
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ bool get_smaf_metadata(int fd, struct mp3entry* id3)
|
|||
id3->composer = NULL;
|
||||
|
||||
id3->vbr = false; /* All SMAF files are CBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
/* check File Chunk and Contents Info Chunk */
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ bool get_spc_metadata(int fd, struct mp3entry* id3)
|
|||
id3->frequency = 32000;
|
||||
id3->length = length+fade;
|
||||
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->genre_string = id3_get_num_genre(36);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
static void read_id3_tags(int fd, struct mp3entry* id3)
|
||||
{
|
||||
id3->title = NULL;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->id3v2len = getid3v2len(fd);
|
||||
id3->tracknum = 0;
|
||||
id3->discnum = 0;
|
||||
|
|
@ -98,7 +98,7 @@ bool get_tta_metadata(int fd, struct mp3entry* id3)
|
|||
id3->length = ((GET_HEADER(ttahdr, DATA_LENGTH)) / id3->frequency) * 1000LL;
|
||||
bps = (GET_HEADER(ttahdr, BITS_PER_SAMPLE));
|
||||
|
||||
datasize = id3->filesize - id3->first_frame_offset;
|
||||
datasize = id3->FS_PREFIX(filesize) - id3->first_frame_offset;
|
||||
origsize = (GET_HEADER(ttahdr, DATA_LENGTH)) * ((bps + 7) / 8) * channels;
|
||||
|
||||
id3->bitrate = (int) ((uint64_t) datasize * id3->frequency * channels * bps
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ bool get_vgm_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->vbr = false;
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
id3->bitrate = 1411;
|
||||
id3->frequency = 44100;
|
||||
|
|
@ -179,7 +179,7 @@ bool get_vgm_metadata(int fd, struct mp3entry* id3)
|
|||
|
||||
/* Seek to gd3 offset and read as
|
||||
many bytes posible */
|
||||
gd3_offset = id3->filesize - (header_size + gd3_offset);
|
||||
gd3_offset = id3->FS_PREFIX(filesize) - (header_size + gd3_offset);
|
||||
if ((lseek(fd, -gd3_offset, SEEK_END) < 0)
|
||||
|| ((read_bytes = read(fd, buf, ID3V2_BUF_SIZE)) <= 0))
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ bool get_vox_metadata(int fd, struct mp3entry* id3)
|
|||
id3->frequency = 8000;
|
||||
id3->bitrate = 8000 * 4 / 1000;
|
||||
id3->vbr = false; /* All VOX files are CBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->length = id3->filesize >> 2;
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
id3->length = id3->FS_PREFIX(filesize) >> 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ bool get_vtx_metadata(int fd, struct mp3entry* id3)
|
|||
if (lseek(fd, 0, SEEK_SET) < 0)
|
||||
goto exit_bad;
|
||||
|
||||
if (filesize(fd) < 20)
|
||||
if (metadata_filesize(fd) < 20)
|
||||
goto exit_bad;
|
||||
|
||||
uint hdr = Reader_ReadWord(fd);
|
||||
|
|
@ -142,7 +142,7 @@ bool get_vtx_metadata(int fd, struct mp3entry* id3)
|
|||
id3->bitrate = 706;
|
||||
id3->frequency = 44100; // XXX allow this to be configured?
|
||||
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = metadata_filesize(fd);
|
||||
id3->length = info.frames * 1000 / info.playerfreq;
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ static bool read_header(int fd, struct mp3entry* id3, const unsigned char *chunk
|
|||
memset(&fmt, 0, sizeof(struct wave_fmt));
|
||||
|
||||
id3->vbr = false; /* All Wave/Wave64 files are CBR */
|
||||
id3->filesize = filesize(fd);
|
||||
id3->FS_PREFIX(filesize) = filesize(fd);
|
||||
|
||||
/* get RIFF chunk header */
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
|
|
@ -376,7 +376,7 @@ static bool read_header(int fd, struct mp3entry* id3, const unsigned char *chunk
|
|||
chunksize += ((is_64)? ((1 + ~chunksize) & 0x07) : (chunksize & 1));
|
||||
|
||||
offset += chunksize;
|
||||
if (offset >= id3->filesize)
|
||||
if (offset >= id3->FS_PREFIX(filesize))
|
||||
break;
|
||||
|
||||
lseek(fd, chunksize - read_data, SEEK_CUR);
|
||||
|
|
@ -400,7 +400,7 @@ static bool read_header(int fd, struct mp3entry* id3, const unsigned char *chunk
|
|||
/* Calculate track length (in ms) and estimate the bitrate (in kbit/s) */
|
||||
id3->length = (fmt.formattag != WAVE_FORMAT_ATRAC3)?
|
||||
(uint64_t)fmt.totalsamples * 1000 / id3->frequency :
|
||||
((id3->filesize - id3->first_frame_offset) * 8) / id3->bitrate;
|
||||
((id3->FS_PREFIX(filesize) - id3->first_frame_offset) * 8) / id3->bitrate;
|
||||
|
||||
/* output header/id3 info (for debug) */
|
||||
DEBUGF("%s header info ----\n", (is_64)? "wave64" : "wave");
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ bool get_wavpack_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->vbr = true; /* All WavPack files are VBR */
|
||||
id3->filesize = filesize (fd);
|
||||
id3->FS_PREFIX(filesize) = filesize (fd);
|
||||
|
||||
/* check up to 16 headers before we give up finding one with audio */
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ bool get_wavpack_metadata(int fd, struct mp3entry* id3)
|
|||
/* if the total number of samples is still unknown, make a guess on the high side (for now) */
|
||||
|
||||
if (totalsamples == (uint32_t) -1) {
|
||||
totalsamples = id3->filesize * 3;
|
||||
totalsamples = id3->FS_PREFIX(filesize) * 3;
|
||||
|
||||
if (!(flags & HYBRID_FLAG))
|
||||
totalsamples /= 2;
|
||||
|
|
@ -144,7 +144,7 @@ bool get_wavpack_metadata(int fd, struct mp3entry* id3)
|
|||
}
|
||||
|
||||
id3->length = ((int64_t) totalsamples * 1000) / id3->frequency;
|
||||
id3->bitrate = id3->filesize / (id3->length / 8);
|
||||
id3->bitrate = id3->FS_PREFIX(filesize) / (id3->length / 8);
|
||||
|
||||
read_ape_tags(fd, id3);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -45,9 +45,11 @@ objcopy = cp $(1) $(1).tmp;mv -f $(1).tmp $(2)
|
|||
else
|
||||
ifdef DEBUG
|
||||
objcopy = cp $(1) $(1).tmp;mv -f $(1).tmp $(2) # objcopy hosted (DEBUG)
|
||||
else ifeq ($(UNAME), Darwin)
|
||||
objcopy = cp $(1) $(1).tmp;mv -f $(1).tmp $(2) # objcopy hosted macOS (GNU objcopy strips LC_ID_DYLIB)
|
||||
else
|
||||
objcopy = $(OC) -S -x $(1) $(2) # objcopy hosted (!DEBUG)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# calculate dependencies for a list of source files $(2) and output them to $(1)
|
||||
|
|
|
|||
|
|
@ -24,18 +24,33 @@ else
|
|||
UIBMP=$(BUILDDIR)/UI256.bmp
|
||||
endif
|
||||
|
||||
# Rust network-stream static library (built from crates/netstream).
|
||||
NETSTREAM_LIB = $(BUILDDIR)/librbnetstream.a
|
||||
NETSTREAM_MANIFEST = $(ROOTDIR)/crates/netstream/Cargo.toml
|
||||
NETSTREAM_CARGO_LIB = $(ROOTDIR)/target/release/librbnetstream.a
|
||||
|
||||
.PHONY: netstream-lib
|
||||
netstream-lib:
|
||||
cargo build --manifest-path $(NETSTREAM_MANIFEST) --release
|
||||
|
||||
$(NETSTREAM_CARGO_LIB): netstream-lib
|
||||
|
||||
$(NETSTREAM_LIB): $(NETSTREAM_CARGO_LIB)
|
||||
$(call PRINTS,CP librbnetstream.a)cp $< $@
|
||||
|
||||
.SECONDEXPANSION: # $$(OBJ) is not populated until after this
|
||||
|
||||
$(SIMLIB): $$(SIMOBJ) $(UIBMP)
|
||||
$(SILENT)$(shell rm -f $@)
|
||||
$(call PRINTS,AR $(@F))$(AR) rcs $@ $(SIMOBJ) >/dev/null
|
||||
|
||||
$(BUILDDIR)/$(BINARY): $$(OBJ) $(FIRMLIB) $(VOICESPEEXLIB) $(CORE_LIBS) $(SIMLIB)
|
||||
$(BUILDDIR)/$(BINARY): $$(OBJ) $(FIRMLIB) $(VOICESPEEXLIB) $(CORE_LIBS) $(SIMLIB) $(NETSTREAM_LIB)
|
||||
ifeq ($(UNAME), Darwin)
|
||||
$(call PRINTS,LD $(BINARY))$(CC) -o $@ $^ $(LDOPTS) $(GLOBAL_LDOPTS) -Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox.map
|
||||
$(call PRINTS,LD $(BINARY))$(CC) -o $@ $^ $(LDOPTS) $(GLOBAL_LDOPTS) -lpthread -ldl \
|
||||
-Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox.map
|
||||
else
|
||||
$(call PRINTS,LD $(BINARY))$(CC) -o $@ -Wl,--start-group $^ -Wl,--end-group $(LDOPTS) $(GLOBAL_LDOPTS) \
|
||||
-Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox.map
|
||||
-lpthread -ldl -Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox.map
|
||||
endif
|
||||
$(SILENT)$(call objcopy,$@,$@)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,25 +35,25 @@ pub const mp3entry = extern struct {
|
|||
id3version: u8,
|
||||
codectype: u32,
|
||||
bitrate: u32,
|
||||
frequency: u32,
|
||||
id3v2len: u32,
|
||||
id3v1len: u32,
|
||||
first_frame_offset: u32,
|
||||
filesize: u32,
|
||||
length: u32,
|
||||
elapsed: u32,
|
||||
frequency: c_ulong,
|
||||
id3v2len: c_ulong,
|
||||
id3v1len: c_ulong,
|
||||
first_frame_offset: c_ulong,
|
||||
filesize: c_ulong,
|
||||
length: c_ulong,
|
||||
elapsed: c_ulong,
|
||||
lead_trim: c_int,
|
||||
tail_trim: c_int,
|
||||
samples: u64,
|
||||
frame_count: u32,
|
||||
bytesperframe: u32,
|
||||
frame_count: c_ulong,
|
||||
bytesperframe: c_ulong,
|
||||
vbr: bool,
|
||||
has_toc: bool,
|
||||
toc: [100]u8,
|
||||
needs_upsampling_correction: bool,
|
||||
id3v2buf: [ID3V2_BUF_SIZE]u8,
|
||||
id3v1buf: [4][92]u8,
|
||||
offset: u32,
|
||||
offset: c_ulong,
|
||||
index: c_int,
|
||||
skip_resume_adjustments: bool,
|
||||
autoresumable: u8,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue