push
This commit is contained in:
565
cache.c
Normal file
565
cache.c
Normal file
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* Copyright 2008-2013 Various Authors
|
||||
* Copyright 2004 Timo Hirvonen
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "cache.h"
|
||||
#include "misc.h"
|
||||
#include "file.h"
|
||||
#include "input.h"
|
||||
#include "track_info.h"
|
||||
#include "utils.h"
|
||||
#include "xmalloc.h"
|
||||
#include "xstrjoin.h"
|
||||
#include "gbuf.h"
|
||||
#include "options.h"
|
||||
#include "pl_env.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#define CACHE_VERSION 0x0d
|
||||
|
||||
#define CACHE_64_BIT 0x01
|
||||
#define CACHE_BE 0x02
|
||||
|
||||
#define CACHE_RESERVED_PATTERN 0xff
|
||||
|
||||
#define CACHE_ENTRY_USED_SIZE 28
|
||||
#define CACHE_ENTRY_RESERVED_SIZE 52
|
||||
#define CACHE_ENTRY_TOTAL_SIZE (CACHE_ENTRY_RESERVED_SIZE + CACHE_ENTRY_USED_SIZE)
|
||||
|
||||
// Cmus Track Cache version X + 4 bytes flags
|
||||
static char cache_header[8] CMUS_NONSTRING = "CTC\0\0\0\0\0";
|
||||
|
||||
// host byte order
|
||||
// mtime is either 32 or 64 bits
|
||||
struct cache_entry {
|
||||
// size of this struct including size itself
|
||||
uint32_t size;
|
||||
|
||||
int32_t play_count;
|
||||
int64_t mtime;
|
||||
int32_t duration;
|
||||
int32_t bitrate;
|
||||
int32_t bpm;
|
||||
|
||||
// when introducing new fields decrease the reserved space accordingly
|
||||
uint8_t _reserved[CACHE_ENTRY_RESERVED_SIZE];
|
||||
|
||||
// filename, codec, codec_profile and N * (key, val)
|
||||
char strings[];
|
||||
};
|
||||
|
||||
// make sure our mmap/sizeof-based code works
|
||||
STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == sizeof(struct cache_entry));
|
||||
STATIC_ASSERT(CACHE_ENTRY_TOTAL_SIZE == offsetof(struct cache_entry, strings));
|
||||
|
||||
|
||||
#define ALIGN(size) (((size) + sizeof(long) - 1) & ~(sizeof(long) - 1))
|
||||
#define HASH_SIZE 1023
|
||||
|
||||
static struct track_info *hash_table[HASH_SIZE];
|
||||
static char *cache_filename;
|
||||
static int total;
|
||||
|
||||
struct fifo_mutex cache_mutex = FIFO_MUTEX_INITIALIZER;
|
||||
|
||||
|
||||
static void add_ti(struct track_info *ti, unsigned int hash)
|
||||
{
|
||||
unsigned int pos = hash % HASH_SIZE;
|
||||
struct track_info *next = hash_table[pos];
|
||||
|
||||
ti->next = next;
|
||||
hash_table[pos] = ti;
|
||||
total++;
|
||||
}
|
||||
|
||||
static int valid_cache_entry(const struct cache_entry *e, unsigned int avail)
|
||||
{
|
||||
unsigned int min_size = sizeof(*e);
|
||||
unsigned int str_size;
|
||||
int i, count;
|
||||
|
||||
if (avail < min_size)
|
||||
return 0;
|
||||
|
||||
if (e->size < min_size || e->size > avail)
|
||||
return 0;
|
||||
|
||||
str_size = e->size - min_size;
|
||||
count = 0;
|
||||
for (i = 0; i < str_size; i++) {
|
||||
if (!e->strings[i])
|
||||
count++;
|
||||
}
|
||||
if (count % 2 == 0)
|
||||
return 0;
|
||||
if (e->strings[str_size - 1])
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static struct track_info *cache_entry_to_ti(struct cache_entry *e)
|
||||
{
|
||||
const char *strings = e->strings;
|
||||
struct track_info *ti;
|
||||
struct keyval *kv;
|
||||
int str_size = e->size - sizeof(*e);
|
||||
int pos, i, count;
|
||||
char *proc_filename;
|
||||
|
||||
if (pl_env_var(strings, NULL) && (proc_filename = pl_env_expand(strings))) {
|
||||
ti = track_info_new(proc_filename);
|
||||
free(proc_filename);
|
||||
} else {
|
||||
ti = track_info_new(strings);
|
||||
}
|
||||
|
||||
ti->duration = e->duration;
|
||||
ti->bitrate = e->bitrate;
|
||||
ti->mtime = e->mtime;
|
||||
ti->play_count = e->play_count;
|
||||
ti->bpm = e->bpm;
|
||||
|
||||
// count strings (filename + codec + codec_profile + key/val pairs)
|
||||
count = 0;
|
||||
for (i = 0; i < str_size; i++) {
|
||||
if (!strings[i])
|
||||
count++;
|
||||
}
|
||||
count = (count - 3) / 2;
|
||||
|
||||
// NOTE: filename already copied by track_info_new()
|
||||
pos = strlen(strings) + 1;
|
||||
ti->codec = strings[pos] ? xstrdup(strings + pos) : NULL;
|
||||
pos += strlen(strings + pos) + 1;
|
||||
ti->codec_profile = strings[pos] ? xstrdup(strings + pos) : NULL;
|
||||
pos += strlen(strings + pos) + 1;
|
||||
kv = xnew(struct keyval, count + 1);
|
||||
for (i = 0; i < count; i++) {
|
||||
int size;
|
||||
|
||||
size = strlen(strings + pos) + 1;
|
||||
kv[i].key = xstrdup(strings + pos);
|
||||
pos += size;
|
||||
|
||||
size = strlen(strings + pos) + 1;
|
||||
kv[i].val = xstrdup(strings + pos);
|
||||
pos += size;
|
||||
}
|
||||
kv[i].key = NULL;
|
||||
kv[i].val = NULL;
|
||||
track_info_set_comments(ti, kv);
|
||||
return ti;
|
||||
}
|
||||
|
||||
struct track_info *lookup_cache_entry(const char *filename, unsigned int hash)
|
||||
{
|
||||
struct track_info *ti = hash_table[hash % HASH_SIZE];
|
||||
|
||||
while (ti) {
|
||||
if (!strcmp(filename, ti->filename))
|
||||
return ti;
|
||||
ti = ti->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void do_cache_remove_ti(struct track_info *ti, unsigned int hash)
|
||||
{
|
||||
unsigned int pos = hash % HASH_SIZE;
|
||||
struct track_info *t = hash_table[pos];
|
||||
struct track_info *next, *prev = NULL;
|
||||
|
||||
while (t) {
|
||||
next = t->next;
|
||||
if (t == ti) {
|
||||
if (prev) {
|
||||
prev->next = next;
|
||||
} else {
|
||||
hash_table[pos] = next;
|
||||
}
|
||||
total--;
|
||||
track_info_unref(ti);
|
||||
return;
|
||||
}
|
||||
prev = t;
|
||||
t = next;
|
||||
}
|
||||
}
|
||||
|
||||
void cache_remove_ti(struct track_info *ti)
|
||||
{
|
||||
do_cache_remove_ti(ti, hash_str(ti->filename));
|
||||
}
|
||||
|
||||
static int read_cache(void)
|
||||
{
|
||||
unsigned int size, offset = 0;
|
||||
struct stat st = {};
|
||||
char *buf;
|
||||
int fd;
|
||||
|
||||
fd = open(cache_filename, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
if (errno == ENOENT)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
fstat(fd, &st);
|
||||
if (st.st_size < sizeof(cache_header))
|
||||
goto close;
|
||||
size = st.st_size;
|
||||
|
||||
buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (buf == MAP_FAILED) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (memcmp(buf, cache_header, sizeof(cache_header)))
|
||||
goto corrupt;
|
||||
|
||||
offset = sizeof(cache_header);
|
||||
while (offset < size) {
|
||||
struct cache_entry *e = (void *)(buf + offset);
|
||||
struct track_info *ti;
|
||||
|
||||
if (!valid_cache_entry(e, size - offset))
|
||||
goto corrupt;
|
||||
|
||||
ti = cache_entry_to_ti(e);
|
||||
add_ti(ti, hash_str(ti->filename));
|
||||
offset += ALIGN(e->size);
|
||||
}
|
||||
munmap(buf, size);
|
||||
close(fd);
|
||||
return 0;
|
||||
corrupt:
|
||||
munmap(buf, size);
|
||||
close:
|
||||
close(fd);
|
||||
// corrupt
|
||||
return -2;
|
||||
}
|
||||
|
||||
int cache_init(void)
|
||||
{
|
||||
unsigned int flags = 0;
|
||||
|
||||
#ifdef WORDS_BIGENDIAN
|
||||
flags |= CACHE_BE;
|
||||
#endif
|
||||
if (sizeof(long) == 8)
|
||||
flags |= CACHE_64_BIT;
|
||||
|
||||
cache_header[7] = flags & 0xff; flags >>= 8;
|
||||
cache_header[6] = flags & 0xff; flags >>= 8;
|
||||
cache_header[5] = flags & 0xff; flags >>= 8;
|
||||
cache_header[4] = flags & 0xff;
|
||||
|
||||
/* assumed version */
|
||||
cache_header[3] = CACHE_VERSION;
|
||||
|
||||
cache_filename = xstrjoin(cmus_config_dir, "/cache");
|
||||
return read_cache();
|
||||
}
|
||||
|
||||
static int ti_filename_cmp(const void *a, const void *b)
|
||||
{
|
||||
const struct track_info *ai = *(const struct track_info **)a;
|
||||
const struct track_info *bi = *(const struct track_info **)b;
|
||||
|
||||
return strcmp(ai->filename, bi->filename);
|
||||
}
|
||||
|
||||
static struct track_info **get_track_infos(bool reference)
|
||||
{
|
||||
struct track_info **tis;
|
||||
int i, c;
|
||||
|
||||
tis = xnew(struct track_info *, total);
|
||||
c = 0;
|
||||
for (i = 0; i < HASH_SIZE; i++) {
|
||||
struct track_info *ti = hash_table[i];
|
||||
|
||||
while (ti) {
|
||||
if (reference)
|
||||
track_info_ref(ti);
|
||||
tis[c++] = ti;
|
||||
ti = ti->next;
|
||||
}
|
||||
}
|
||||
qsort(tis, total, sizeof(struct track_info *), ti_filename_cmp);
|
||||
return tis;
|
||||
}
|
||||
|
||||
static void flush_buffer(int fd, struct gbuf *buf)
|
||||
{
|
||||
if (buf->len) {
|
||||
write_all(fd, buf->buffer, buf->len);
|
||||
gbuf_clear(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void write_ti(int fd, struct gbuf *buf, struct track_info *ti, unsigned int *offsetp)
|
||||
{
|
||||
char *proc_filename = pl_env_reduce(ti->filename);
|
||||
const struct keyval *kv = ti->comments;
|
||||
unsigned int offset = *offsetp;
|
||||
unsigned int pad;
|
||||
struct cache_entry e;
|
||||
int *len, alloc = 64, count, i;
|
||||
|
||||
memset(e._reserved, CACHE_RESERVED_PATTERN, sizeof(e._reserved));
|
||||
|
||||
count = 0;
|
||||
len = xnew(int, alloc);
|
||||
e.size = sizeof(e);
|
||||
e.duration = ti->duration;
|
||||
e.bitrate = ti->bitrate;
|
||||
e.mtime = ti->mtime;
|
||||
e.play_count = ti->play_count;
|
||||
e.bpm = ti->bpm;
|
||||
len[count] = strlen(proc_filename) + 1;
|
||||
e.size += len[count++];
|
||||
len[count] = (ti->codec ? strlen(ti->codec) : 0) + 1;
|
||||
e.size += len[count++];
|
||||
len[count] = (ti->codec_profile ? strlen(ti->codec_profile) : 0) + 1;
|
||||
e.size += len[count++];
|
||||
for (i = 0; kv[i].key; i++) {
|
||||
if (count + 2 > alloc) {
|
||||
alloc *= 2;
|
||||
len = xrenew(int, len, alloc);
|
||||
}
|
||||
len[count] = strlen(kv[i].key) + 1;
|
||||
e.size += len[count++];
|
||||
len[count] = strlen(kv[i].val) + 1;
|
||||
e.size += len[count++];
|
||||
}
|
||||
|
||||
pad = ALIGN(offset) - offset;
|
||||
if (gbuf_avail(buf) < pad + e.size)
|
||||
flush_buffer(fd, buf);
|
||||
|
||||
count = 0;
|
||||
if (pad)
|
||||
gbuf_set(buf, 0, pad);
|
||||
gbuf_add_bytes(buf, &e, sizeof(e));
|
||||
gbuf_add_bytes(buf, proc_filename, len[count++]);
|
||||
gbuf_add_bytes(buf, ti->codec ? ti->codec : "", len[count++]);
|
||||
gbuf_add_bytes(buf, ti->codec_profile ? ti->codec_profile : "", len[count++]);
|
||||
for (i = 0; kv[i].key; i++) {
|
||||
gbuf_add_bytes(buf, kv[i].key, len[count++]);
|
||||
gbuf_add_bytes(buf, kv[i].val, len[count++]);
|
||||
}
|
||||
|
||||
free(len);
|
||||
*offsetp = offset + pad + e.size;
|
||||
|
||||
free(proc_filename);
|
||||
}
|
||||
|
||||
int cache_close(void)
|
||||
{
|
||||
GBUF(buf);
|
||||
struct track_info **tis;
|
||||
unsigned int offset;
|
||||
int i, fd, rc;
|
||||
char *tmp;
|
||||
|
||||
tmp = xstrjoin(cmus_config_dir, "/cache.tmp");
|
||||
fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0666);
|
||||
if (fd < 0) {
|
||||
free(tmp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tis = get_track_infos(false);
|
||||
|
||||
gbuf_grow(&buf, 64 * 1024 - 1);
|
||||
gbuf_add_bytes(&buf, cache_header, sizeof(cache_header));
|
||||
offset = sizeof(cache_header);
|
||||
for (i = 0; i < total; i++)
|
||||
write_ti(fd, &buf, tis[i], &offset);
|
||||
flush_buffer(fd, &buf);
|
||||
gbuf_free(&buf);
|
||||
free(tis);
|
||||
|
||||
close(fd);
|
||||
rc = rename(tmp, cache_filename);
|
||||
free(tmp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static struct track_info *ip_get_ti(const char *filename)
|
||||
{
|
||||
struct track_info *ti = NULL;
|
||||
struct input_plugin *ip;
|
||||
struct keyval *comments;
|
||||
int rc;
|
||||
|
||||
ip = ip_new(filename);
|
||||
rc = ip_open(ip);
|
||||
if (rc) {
|
||||
ip_delete(ip);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rc = ip_read_comments(ip, &comments);
|
||||
if (!rc) {
|
||||
ti = track_info_new(filename);
|
||||
track_info_set_comments(ti, comments);
|
||||
ti->duration = ip_duration(ip);
|
||||
ti->bitrate = ip_bitrate(ip);
|
||||
ti->codec = ip_codec(ip);
|
||||
ti->codec_profile = ip_codec_profile(ip);
|
||||
ti->mtime = ip_is_remote(ip) ? -1 : file_get_mtime(filename);
|
||||
}
|
||||
ip_delete(ip);
|
||||
return ti;
|
||||
}
|
||||
|
||||
struct track_info *cache_get_ti(const char *filename, int force)
|
||||
{
|
||||
unsigned int hash = hash_str(filename);
|
||||
struct track_info *ti;
|
||||
int reload = 0;
|
||||
|
||||
if (pl_env_var(filename, NULL)) {
|
||||
struct growing_keyvals c = {NULL, 0, 0};
|
||||
keyvals_terminate(&c);
|
||||
|
||||
ti = track_info_new(filename);
|
||||
ti->duration = 0;
|
||||
track_info_set_comments(ti, c.keyvals);
|
||||
|
||||
track_info_ref(ti);
|
||||
return ti;
|
||||
}
|
||||
|
||||
ti = lookup_cache_entry(filename, hash);
|
||||
if (ti) {
|
||||
if ((!skip_track_info && ti->duration == 0 && !is_http_url(filename)) || force){
|
||||
do_cache_remove_ti(ti, hash);
|
||||
ti = NULL;
|
||||
reload = 1;
|
||||
}
|
||||
}
|
||||
if (!ti) {
|
||||
if (skip_track_info && !reload && !force) {
|
||||
struct growing_keyvals c = {NULL, 0, 0};
|
||||
|
||||
ti = track_info_new(filename);
|
||||
|
||||
keyvals_terminate(&c);
|
||||
track_info_set_comments(ti, c.keyvals);
|
||||
|
||||
ti->duration = 0;
|
||||
} else {
|
||||
ti = ip_get_ti(filename);
|
||||
}
|
||||
if (!ti)
|
||||
return NULL;
|
||||
add_ti(ti, hash);
|
||||
}
|
||||
track_info_ref(ti);
|
||||
return ti;
|
||||
}
|
||||
|
||||
struct track_info **cache_refresh(int *count, int force)
|
||||
{
|
||||
struct track_info **tis = get_track_infos(true);
|
||||
int i, n = total;
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned int hash;
|
||||
struct track_info *ti = tis[i];
|
||||
struct stat st;
|
||||
int rc = 0;
|
||||
|
||||
cache_yield();
|
||||
|
||||
/*
|
||||
* If no-one else has reference to tis[i] then it is set to NULL
|
||||
* otherwise:
|
||||
*
|
||||
* unchanged: tis[i] = NULL
|
||||
* deleted: tis[i]->next = NULL
|
||||
* changed: tis[i]->next = new
|
||||
*/
|
||||
|
||||
if (!is_url(ti->filename)) {
|
||||
rc = stat(ti->filename, &st);
|
||||
if (!rc && !force && ti->mtime == st.st_mtime) {
|
||||
// unchanged
|
||||
track_info_unref(ti);
|
||||
tis[i] = NULL;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
hash = hash_str(ti->filename);
|
||||
do_cache_remove_ti(ti, hash);
|
||||
|
||||
if (!rc) {
|
||||
// changed
|
||||
struct track_info *new_ti;
|
||||
|
||||
// clear cache-only entries
|
||||
if (force && track_info_unique_ref(ti)) {
|
||||
track_info_unref(ti);
|
||||
tis[i] = NULL;
|
||||
continue;
|
||||
}
|
||||
|
||||
new_ti = ip_get_ti(ti->filename);
|
||||
if (new_ti) {
|
||||
add_ti(new_ti, hash);
|
||||
|
||||
if (track_info_unique_ref(ti)) {
|
||||
track_info_unref(ti);
|
||||
tis[i] = NULL;
|
||||
} else {
|
||||
track_info_ref(new_ti);
|
||||
ti->next = new_ti;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// treat as deleted
|
||||
}
|
||||
|
||||
// deleted
|
||||
if (track_info_unique_ref(ti)) {
|
||||
track_info_unref(ti);
|
||||
tis[i] = NULL;
|
||||
} else {
|
||||
ti->next = NULL;
|
||||
}
|
||||
}
|
||||
*count = n;
|
||||
return tis;
|
||||
}
|
||||
Reference in New Issue
Block a user