disco-dl

Discography/album downloader
git clone git://git.wimdupont.com/disco-dl.git
Log | Files | Refs

commit 5071e3f70e179ef55f50c88db738103a16d8f574
Author: Wim Dupont <wim@wimdupont.com>
Date:   Sat,  1 Jun 2024 10:45:50 +0200

init

Diffstat:
A.gitignore | 1+
AMakefile | 43+++++++++++++++++++++++++++++++++++++++++++
AREADME.adoc | 17+++++++++++++++++
Aconfig.h | 2++
Adisco-dl | 0
Adisco-dl.1 | 18++++++++++++++++++
Adisco-dl.c | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adisco-dl.h | 39+++++++++++++++++++++++++++++++++++++++
Aexample.csv | 1+
9 files changed, 543 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/Makefile b/Makefile @@ -0,0 +1,43 @@ +CC = gcc + +NAME = disco-dl +VERSION = 0.1 + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +BIN = disco-dl +SRC = ${BIN:=.c} +OBJ = ${SRC:.c=.o} +MAN1 = ${BIN:=.1} + +all: ${BIN} + +${BIN}: ${@:=.o} + +${OBJ}: config.h disco-dl.h + +.o: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + rm -f ${BIN} ${OBJ} + +config.h: + cp config.h $@ + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f ${BIN} "${DESTDIR}${PREFIX}/bin" + chmod 755 "${DESTDIR}${PREFIX}/bin/${BIN}" + mkdir -p "${DESTDIR}${MANPREFIX}/man1" + sed "s/VERSION/${VERSION}/g" < ${MAN1} > "${DESTDIR}${MANPREFIX}/man1/${MAN1}" + chmod 644 "${DESTDIR}${MANPREFIX}/man1/${MAN1}" + +uninstall: + rm -f \ + "${DESTDIR}${PREFIX}/bin/${BIN}"\ + "${DESTDIR}${MANPREFIX}/man1/${MAN1}" + +.PHONY: all clean install uninstall diff --git a/README.adoc b/README.adoc @@ -0,0 +1,17 @@ += DISCO-DL + +Discography/album downloader based on .csv file. + +== Installation + +[source,bash] +---- +$ make clean install +---- + +== Required software + +* https://github.com/yt-dlp/yt-dlp[yt-dlp] +* https://git.ffmpeg.org/ffmpeg.git[ffmpeg] +* https://github.com/squell/id3[id3] + diff --git a/config.h b/config.h @@ -0,0 +1,2 @@ +#define FILENAME "example.csv" +#define ROOT_DIR "/tmp" diff --git a/disco-dl b/disco-dl Binary files differ. diff --git a/disco-dl.1 b/disco-dl.1 @@ -0,0 +1,18 @@ +.TH DISCO-DL 1 disco-dl-VERSION +.SH NAME +disco-dl \- discography/album downloader +.SH SYNOPSIS +.B disco-dl +.SH DESCRIPTION +.B disco-dl +is a discography downloader based on .csv file using \fByt-dlp\fR for downloading the files, +\fBffmepg\fR for convertering to .mp3 files, and \fBid3\fR for tagging metadata. +.SH CONFIGURATION +.SS PROPERTIES +define properties configuration in the \fBconfig.h\fR file +.SS CSV FILE FORMAT +band|album|genre|year|url|song1;song2;song3; +.SH SEE ALSO +\&\fByt-dlp\fR\|(1), +\&\fBffmpeg\fR\|(1), +\&\fBid3\fR\|(1) diff --git a/disco-dl.c b/disco-dl.c @@ -0,0 +1,422 @@ +#include <stdarg.h> +#include <unistd.h> +#include <errno.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <stdio.h> +#include <string.h> +#include <libgen.h> +#include <fts.h> + +#include "config.h" +#include "disco-dl.h" + +int +main(void) +{ + int line_count = 0; + Album **albums; + + albums = get_albums(&line_count); + + for (int i = 0; i < line_count; i++) { + dl_album(albums[i]); + tag_album(albums[i]); + + free(albums[i]->band); + free(albums[i]->album); + free(albums[i]->genre); + free(albums[i]->url); + free(albums[i]->tracklist); + free(albums[i]->dir); + free(albums[i]); + } + free(albums); + + return EXIT_SUCCESS; +} + +void +dl_album(Album *album) +{ + char *genredir = make_message("%s%s%s", ROOT_DIR, DIR_SEP, album->genre); + char *banddir = make_message("%s%s%s", genredir, DIR_SEP, album->band); + char *albumdir = make_message("%s%s%s", banddir, DIR_SEP, album->album); + char *sys_command; + + make_dir(ROOT_DIR); + make_dir(genredir); + make_dir(banddir); + int status = make_dir(albumdir); + if (status == 1) + fprintf(stdout, "Pathname already exists: %s\n", albumdir); + status = chdir(albumdir); + if (status != 0) + exit(status); + + album->dir = albumdir; + + + sys_command = make_message("yt-dlp -x -f bestaudio -i -o \"%s/%(playlist_index)s - %(title)s.%(ext)s\" \"%s\"", albumdir, album->url); + system(sys_command); + + free(sys_command); + free(genredir); + free(banddir); +} + +void +tag_album(Album *album) +{ + char *token; + char *tracklist; + unsigned int count = 0; + Track *track; + + if (album->tracklist && *album->tracklist != '\0') { + tracklist = album->tracklist; + while ((token = strsep(&tracklist, ";")) != NULL) { + track = get_track(&album, &token, ++count); + if (track != NULL) { + if (track->title != NULL) { + printf("%s - %s\n", track->tracknum, track->title); + convert(track); + /* TODO: fix tagging implementation and remove from convert() + id3_tag(track); + */ + } + free(track->path); + free(track->tracknum); + free(track); + } + } + + free(token); + free(tracklist); + } +} + +Track * +get_track(Album **album, char **track_name, unsigned int count) +{ + char *filename; + char *tracknumber; + FTS *ftsp; + FTSENT *p, *chp; + int fts_options = FTS_COMFOLLOW | FTS_LOGICAL | FTS_NOCHDIR; + char *pp[] = { (*album)->dir, NULL }; + Track *track = NULL; + char *substr; + + + if ((ftsp = fts_open(pp, fts_options, NULL)) == NULL) + fatal("Error during fts_open %s\n", pp); + + chp = fts_children(ftsp, FTS_NAMEONLY); + + if (chp == NULL) + fatal("Error during fts_children %s\n", pp); + + while ((p = fts_read(ftsp)) != NULL) { + switch (p->fts_info) { + case FTS_F: + filename = basename(p->fts_path); + substr = strstr(filename, " "); + if (substr == NULL) + break; + tracknumber = calloc(3, sizeof(char)); + memcpy(tracknumber, filename, substr - filename); + if (atoi(tracknumber) == count) { + track = (Track*) malloc(sizeof(Track)); + if (track == NULL) + fatal("Fatal: failed to allocate bytes for track.\n"); + track->path = (char*) malloc(strlen(p->fts_path) + 1); + if (track->path == NULL) + fatal("Fatal: failed to allocate bytes for track->path.\n"); + track->title = *track_name; + track->tracknum = tracknumber; + track->album = *album; + strcpy(track->path, p->fts_path); + goto end; + } + free(tracknumber); + + break; + default: + break; + } + } + end: + + fts_close(ftsp); + + return track; +} + +void +convert(Track *track){ + char *sys_command; + char *path; + char *base; + + base = strdup(track->path); + base = dirname(base); + + path = make_message("%s%s%s - %s.mp3", base, DIR_SEP, track->tracknum, track->title); + sys_command = make_message("ffmpeg -loglevel error -i \"%s\" \"%s\"", track->path, path); + system(sys_command); + free(sys_command); + + remove(track->path); + free(track->path); + track->path = strdup(path); + + sys_command = make_message("id3 -t '%s' -a '%s' -l '%s' -y '%d' -n '%s' -g '%s' '%s'", + track->title, + track->album->band, + track->album->album, + track->album->year, + track->tracknum, + track->album->genre, + track->path); + system(sys_command); + + free(path); + free(base); + free(sys_command); +} + +Album ** +get_albums(int *line_count) +{ + char *line_buf = NULL; + size_t line_buf_size = 0; + ssize_t line_size; + FILE *fp = fopen(FILENAME, "r"); + Album **albums = NULL; + + if (!fp) + fatal("Error opening file '%s'\n", FILENAME); + + line_size = getline(&line_buf, &line_buf_size, fp); + albums = (Album**) malloc(sizeof(Album)); + + if (albums == NULL) + fatal("Fatal: failed to allocate bytes for albums.\n"); + + if (line_size <= 0) + fatal("File '%s' is empty\n", FILENAME); + + while (line_size >= 0) { + albums = (Album**) realloc(albums, (sizeof(Album) * (*line_count + 1))); + if (albums == NULL) + fatal("Fatal: failed to reallocate bytes for albums.\n"); + + albums[*line_count] = get_album(line_buf, line_size); + + line_size = getline(&line_buf, &line_buf_size, fp); + (*line_count)++; + } + + free(line_buf); + fclose(fp); + + return albums; +} + +Album * +get_album(char *line_buf, ssize_t line_size) +{ + Album *album = (Album*) malloc(sizeof(Album)); + if (album == NULL) + fatal("Fatal: failed to allocate bytes for album.\n"); + + album->band = (char*) malloc(line_size); + album->album = (char*) malloc(line_size); + album->genre = (char*) malloc(line_size); + album->url = (char*) malloc(line_size); + album->tracklist = (char*) malloc(line_size); + + if (album->band == NULL | album->album == NULL | album->genre == NULL | album->url == NULL | album->tracklist == NULL) + fatal("Fatal: failed to allocate bytes for album.\n"); + + sscanf(line_buf, + "%[^|]|%[^|]|%[^|]|%d|%[^|]|%[^0]", + album->band, + album->album, + album->genre, + &album->year, + album->url, + album->tracklist); + + return album; +} + +int +make_dir(const char * name) +{ + int status = EXIT_SUCCESS; + errno = 0; + int ret = mkdir(name, S_IRWXU); + if (ret == -1) { + switch (errno) { + case EACCES: + fatal("The parent directory does not allow write: %s\n", name); + case EEXIST: + status = 1; + break; + case ENAMETOOLONG: + fatal("Pathname is too long: %s\n", name); + default: + fatal("mkdir error: %s\n", name); + } + } + + return status; +} + +char * +concat(const char *s1, const char *s2) +{ + char *result = malloc(strlen(s1) + strlen(s2) + 1); + if (result == NULL) + fatal("Fatal: failed to allocate bytes for concat.\n"); + + strcpy(result, s1); + strcat(result, s2); + + return result; +} + +char * +make_message(const char *fmt, ...) +{ + int n = 0; + size_t size = 0; + char *p = NULL; + va_list ap; + + va_start(ap, fmt); + n = vsnprintf(p, size, fmt, ap); + va_end(ap); + + if (n < 0) + return NULL; + + size = (size_t) n + 1; + p = malloc(size); + if (p == NULL) + return NULL; + + va_start(ap, fmt); + n = vsnprintf(p, size, fmt, ap); + va_end(ap); + + if (n < 0) { + free(p); + return NULL; + } + + return p; +} + +void +id3_tag(Track *track) +{ + char *tagfile = "taginf.txt"; + char yearstr[5]; + unsigned char pad[7] = { 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76 }; + + int len = snprintf(yearstr, 5, "%d", track->album->year); + FILE* fp; + fp = fopen(tagfile, "wb"); + if (fp == NULL) + fatal("Failed to open file %s\n", tagfile); + + fprintf(fp, "ID3"); + fwrite(pad, sizeof(pad), 1, fp); + + tag("TRCK", track->tracknum, &fp); + tag("TIT2", track->title, &fp); + tag("TYER", yearstr, &fp); + tag("TPE1", track->album->band, &fp); + tag("TALB", track->album->album, &fp); + tag("TCON", track->album->genre, &fp); + + fclose(fp); + merge_file(tagfile, track->path); +} + +void tag +(char *tag, char *value, FILE **f1) +{ + int size = 0; + unsigned char pad[3] = { 0x00, 0x00, 0x00 }; + + fprintf(*f1, tag); + fwrite(pad, sizeof(pad), 1, *f1); + size = strlen(value) + 1; + fprintf(*f1, "%c", size); + fwrite(pad, sizeof(pad), 1, *f1); + fprintf(*f1, "%s", value); +} + +void +merge_file(char *prefile, char *file) +{ + FILE *f1, *f2, *f3; + char *tmp = "tmp.txt"; + int i = 0; + int ch; + f1 = fopen(prefile, "rb"); + f2 = fopen(file, "rb"); + f3 = fopen(tmp, "wb"); + + if (f1 == NULL) + fatal("Failed to open file %s\n", prefile); + if (f2 == NULL) + fatal("Failed to open file %s\n", file); + if (f3 == NULL) + fatal("Failed to open file %s\n", tmp); + + /* + while ((ch = fgetc(f2)) != EOF && ++i <= 34) + fputc(ch, f3); + + while ((ch = fgetc(f1)) != EOF) + fputc(ch, f3); + + i = 0; + while ((ch = fgetc(f2)) != EOF) { + if (++i > 34) + fputc(ch, f3); + } + */ + + while ((ch = fgetc(f1)) != EOF) + fputc(ch, f3); + + while ((ch = fgetc(f2)) != EOF) + fputc(ch, f3); + + fclose(f1); + fclose(f2); + fclose(f3); + + rename(tmp, file); + remove(prefile); + remove(tmp); +} + +void +fatal(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + exit(EXIT_FAILURE); +} + diff --git a/disco-dl.h b/disco-dl.h @@ -0,0 +1,39 @@ +#ifndef DISCODL_H +#define DISCODL_H + +#define DIR_SEP "/" + +typedef struct +{ + char *band; + char *album; + char *genre; + int year; + char *url; + char *tracklist; + char *dir; +} Album; + +typedef struct +{ + char *tracknum; + char *title; + char *path; + Album *album; +} Track; + +int make_dir(const char *name); +char *concat(const char *s1, const char *s2); +Track *get_track(Album **album, char **track_name, unsigned int count); +char *make_message(const char *str, ...); +Album **get_albums(int *line_count); +Album *get_album(char *line_buf, ssize_t line_size); +void tag_album(Album *album); +void dl_album(Album *album); +void id3_tag(Track *track); +void tag(char *tag, char *value, FILE **f1); +void merge_file(char *prefile1, char *file2); +void convert(Track *track); +void fatal(const char *fmt, ...); + +#endif diff --git a/example.csv b/example.csv @@ -0,0 +1 @@ +Invent Animate|Stillworld|Metal|2016|https://www.youtube.com/playlist?list=PLgtvGkabBTgh7xGz6O0GCIAQCnOcpmWlw|Indigo;Agoraaaa;White Wolf;Celestial Floods;Solace;Dead Roots;Vacant;Midnight Hymn;Darkbloom;Soul Sleep;