ref: 9f63a97f4e9d7768ac3b0923d50f43d2d62e2874
dir: /mkplist.c/
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <tags.h>
#include "plist.h"
#include "icy.c"
enum
{
Maxname = 256+2, /* seems enough? */
Maxdepth = 16, /* max recursion depth */
};
#define MAX(a, b) (a > b ? a : b)
static Biobuf *bf;
static Meta *curr;
static Meta *all;
static int numall;
static int firstiscomposer;
static int keepfirstartist;
static char *fmts[] = {
[Fmp3] = "mp3",
[Fogg] = "ogg",
[Fflac] = "flac",
[Fm4a] = "m4a",
[Fopus] = "opus",
};
static Meta *
newmeta(void)
{
if(numall == 0){
free(all);
all = nil;
}
if(all == nil)
all = mallocz(sizeof(Meta), 1);
else if((numall & (numall-1)) == 0)
all = realloc(all, numall*2*sizeof(Meta));
if(all == nil)
return nil;
memset(&all[numall++], 0, sizeof(Meta));
return &all[numall-1];
}
static void
cb(Tagctx *ctx, int t, const char *k, const char *v, int offset, int size, Tagread f)
{
int i, iscomposer;
USED(ctx);
switch(t){
case Tartist:
if(curr->numartist < Maxartist){
iscomposer = strcmp(k, "TCM") == 0 || strcmp(k, "TCOM") == 0;
/* prefer lead performer/soloist, helps when TP2/TPE2 is the first one and is set to "VA" */
/* always put composer first, if available */
if(iscomposer || (!keepfirstartist && (strcmp(k, "TP1") == 0 || strcmp(k, "TPE1") == 0))) {
if(curr->numartist > 0)
curr->artist[curr->numartist] = curr->artist[curr->numartist-1];
curr->artist[0] = strdup(v);
curr->numartist++;
keepfirstartist = 1;
firstiscomposer = iscomposer;
return;
}
for(i = 0; i < curr->numartist; i++){
if(cistrcmp(curr->artist[i], v) == 0)
return;
}
curr->artist[curr->numartist++] = strdup(v);
}
break;
case Talbum:
if(curr->album == nil)
curr->album = strdup(v);
break;
case Ttitle:
if(curr->title == nil)
curr->title = strdup(v);
break;
case Tdate:
if(curr->date == nil)
curr->date = strdup(v);
break;
case Ttrack:
if(curr->track == nil)
curr->track = strdup(v);
break;
case Timage:
if(curr->imagefmt == nil){
curr->imagefmt = strdup(v);
curr->imageoffset = offset;
curr->imagesize = size;
curr->imagereader = f != nil;
}
break;
}
}
static int
ctxread(Tagctx *ctx, void *buf, int cnt)
{
USED(ctx);
return Bread(bf, buf, cnt);
}
static int
ctxseek(Tagctx *ctx, int offset, int whence)
{
USED(ctx);
return Bseek(bf, offset, whence);
}
static char buf[4096];
static Tagctx ctx =
{
.read = ctxread,
.seek = ctxseek,
.tag = cb,
.buf = buf,
.bufsz = sizeof(buf),
.aux = nil,
};
static void
scanfile(char *path)
{
int res;
char *s;
if((bf = Bopen(path, OREAD)) == nil){
fprint(2, "%s: %r\n", path);
return;
}
if((curr = newmeta()) == nil)
sysfatal("no memory");
firstiscomposer = keepfirstartist = 0;
res = tagsget(&ctx);
if(ctx.format != Funknown){
if(res != 0)
fprint(2, "%s: no tags\n", path);
}else{
numall--;
Bterm(bf);
return;
}
if(ctx.duration == 0)
fprint(2, "%s: no duration\n", path);
if(curr->title == nil){
if((s = utfrrune(path, '/')) == nil)
s = path;
curr->title = strdup(s+1);
}
curr->path = strdup(path);
curr->duration = ctx.duration;
if(ctx.format >= nelem(fmts))
sysfatal("mkplist needs a rebuild with updated libtags");
curr->filefmt = fmts[ctx.format];
Bterm(bf);
}
static int
scan(char **dir, int depth)
{
char *path;
Dir *buf, *d;
long n;
int dirfd, len;
if((dirfd = open(*dir, OREAD)) < 0)
sysfatal("%s: %r", *dir);
len = strlen(*dir);
if((*dir = realloc(*dir, len+1+Maxname)) == nil)
sysfatal("no memory");
path = *dir;
path[len] = '/';
for(n = 0, buf = nil; n >= 0;){
if((n = dirread(dirfd, &buf)) < 0){
path[len] = 0;
scanfile(path);
break;
}
if(n == 0){
free(buf);
break;
}
for(d = buf; n > 0; n--, d++){
if(strcmp(d->name, ".") == 0 || strcmp(d->name, "..") == 0)
continue;
path[len+1+Maxname-2] = 0;
strncpy(&path[len+1], d->name, Maxname);
if(path[len+1+Maxname-2] != 0)
sysfatal("Maxname=%d was a bad choice", Maxname);
if((d->mode & DMDIR) == 0){
scanfile(path);
}else if(depth < Maxdepth){ /* recurse into the directory */
scan(dir, depth+1);
path = *dir;
}else{
fprint(2, "%s: too deep\n", path);
}
}
free(buf);
}
close(dirfd);
return 0;
}
static void
printmeta(Meta *m, int index)
{
static char buf[4096];
char *s;
int i;
buf[sizeof(buf)-2] = 0;
s = buf;
for(i = 0; i < m->numartist; i++)
s = seprint(s, buf+sizeof(buf), "%c%ld %s\n", Partist, strlen(m->artist[i]), m->artist[i]);
if(m->album != nil)
s = seprint(s, buf+sizeof(buf), "%c%ld %s\n", Palbum, strlen(m->album), m->album);
if(m->title != nil)
s = seprint(s, buf+sizeof(buf), "%c%ld %s\n", Ptitle, strlen(m->title), m->title);
if(m->date != nil)
s = seprint(s, buf+sizeof(buf), "%c%ld %s\n", Pdate, strlen(m->date), m->date);
if(m->track != nil)
s = seprint(s, buf+sizeof(buf), "%c%ld %s\n", Ptrack, strlen(m->track), m->track);
if(m->duration > 0)
s = seprint(s, buf+sizeof(buf), "%c %d\n", Pduration, m->duration);
if(m->imagesize > 0)
s = seprint(s, buf+sizeof(buf), "%c %d %d %d %s\n", Pimage, m->imageoffset, m->imagesize, m->imagereader, m->imagefmt);
s = seprint(s, buf+sizeof(buf), "%c %s\n%c %s\n", Pfilefmt, m->filefmt, Ppath, m->path);
if(buf[sizeof(buf)-2] != 0)
sysfatal("buffer size of %d bytes was a bad choice", sizeof(buf));
print("%c %d %d\n%s", Precord, index, (int)(s-buf), buf);
}
static int
cmpmeta(void *a_, void *b_)
{
Meta *a, *b;
char *ae, *be;
int i, x;
a = a_;
b = b_;
ae = utfrrune(a->path, '/');
be = utfrrune(b->path, '/');
if(ae != nil && be != nil && (x = cistrncmp(a->path, b->path, MAX(ae-a->path, be-b->path))) != 0) /* different path */
return x;
/* same path, must be the same album/cd, but first check */
for(i = 0; i < a->numartist && i < b->numartist; i++){
if((x = cistrcmp(a->artist[i], b->artist[i])) != 0){
if(a->album != nil && b->album != nil && cistrcmp(a->album, b->album) != 0)
return x;
}
}
if(a->date != nil || b->date != nil){
if(a->date == nil && b->date != nil) return -1;
if(a->date != nil && b->date == nil) return 1;
if((x = atoi(a->date) - atoi(b->date)) != 0) return x;
}else if(a->album != nil || b->album != nil){
if(a->album == nil && b->album != nil) return -1;
if(a->album != nil && b->album == nil) return 1;
if((x = cistrcmp(a->album, b->album)) != 0) return x;
}
if(a->track != nil || b->track != nil){
if(a->track == nil && b->track != nil) return -1;
if(a->track != nil && b->track == nil) return 1;
if((x = atoi(a->track) - atoi(b->track)) != 0) return x;
}
return cistrcmp(a->path, b->path);
}
void
main(int argc, char **argv)
{
char *dir;
int i;
if(argc < 2){
fprint(2, "usage: mkplist DIR [DIR2 ...] > noise.plist\n");
exits("usage");
}
for(i = 1; i < argc; i++){
if(strncmp(argv[i], "http://", 7) == 0 || strncmp(argv[i], "https://", 8) == 0){
if((curr = newmeta()) == nil)
sysfatal("no memory");
curr->title = argv[i];
curr->path = argv[i];
curr->filefmt = "";
if(icyfill(curr) != 0)
fprint(2, "%s: %r\n", argv[i]);
}else{
dir = strdup(argv[i]);
scan(&dir, 0);
}
}
qsort(all, numall, sizeof(Meta), cmpmeta);
print("# %d\n", numall);
for(i = 0; i < numall; i++){
if(all[i].numartist < 1)
fprint(2, "no artists: %s\n", all[i].path);
if(all[i].title == nil)
fprint(2, "no title: %s\n", all[i].path);
printmeta(&all[i], i);
}
fprint(2, "found %d tagged tracks\n", numall);
exits(nil);
}