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;