commit 5071e3f70e179ef55f50c88db738103a16d8f574
Author: Wim Dupont <wim@wimdupont.com>
Date: Sat, 1 Jun 2024 10:45:50 +0200
init
Diffstat:
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;