diff --git a/capture.c b/capture.c new file mode 100644 index 0000000..b7a3362 --- /dev/null +++ b/capture.c @@ -0,0 +1,310 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int callSound(char* path) +{ + int child = fork(); + if (child == 0) + { + char finalPath[60]; + strcpy(finalPath, getenv("HOME")); + strcat(finalPath, "/.local/share/sounds/"); + strcat(finalPath, path); + + execlp("ffmpeg", "ffmpeg", "-loglevel", "error", "-i", finalPath, "-f", "alsa", "default", NULL); + } + else if (child < 0) + { + printf("Could not generate sound %s\n", path); + exit(5); + } + else + { + return child; + } +} + +void callErrorSound(int code) +{ + int returnCode = callSound("error.wav"); + wait(NULL); + for (int i = 0; i < code; i++) + { + returnCode = callSound("bit.wav"); + wait(NULL); + } +} + +int main(int argc, char **argv) +{ + const int NUM_KEYS = 249; + const int ARG_LENGTH = 20; + const int MAX_ARGS = 20; + char validChars[NUM_KEYS][2]; + for (int i = 0; i < NUM_KEYS; i++) + { + switch (i) + { + case KEY_0: + validChars[i][0] = '0'; + validChars[i][1] = '0'; + break; + case KEY_1: + validChars[i][0] = '1'; + validChars[i][1] = '1'; + break; + case KEY_2: + validChars[i][0] = '2'; + validChars[i][1] = '2'; + break; + case KEY_3: + validChars[i][0] = '3'; + validChars[i][1] = '3'; + break; + case KEY_4: + validChars[i][0] = '4'; + validChars[i][1] = '4'; + break; + case KEY_5: + validChars[i][0] = '5'; + validChars[i][1] = '5'; + break; + case KEY_6: + validChars[i][0] = '6'; + validChars[i][1] = '6'; + break; + case KEY_7: + validChars[i][0] = '7'; + validChars[i][1] = '7'; + break; + case KEY_8: + validChars[i][0] = '8'; + validChars[i][1] = '8'; + break; + case KEY_9: + validChars[i][0] = '9'; + validChars[i][1] = '9'; + break; + case KEY_A: + validChars[i][0] = 'A'; + validChars[i][1] = 'a'; + break; + case KEY_B: + validChars[i][0] = 'B'; + validChars[i][1] = 'b'; + break; + case KEY_C: + validChars[i][0] = 'C'; + validChars[i][1] = 'c'; + break; + case KEY_D: + validChars[i][0] = 'D'; + validChars[i][1] = 'd'; + break; + case KEY_E: + validChars[i][0] = 'E'; + validChars[i][1] = 'e'; + break; + case KEY_F: + validChars[i][0] = 'F'; + validChars[i][1] = 'f'; + break; + case KEY_G: + validChars[i][0] = 'G'; + validChars[i][1] = 'g'; + break; + case KEY_H: + validChars[i][0] = 'H'; + validChars[i][1] = 'h'; + break; + case KEY_I: + validChars[i][0] = 'I'; + validChars[i][1] = 'i'; + break; + case KEY_J: + validChars[i][0] = 'J'; + validChars[i][1] = 'j'; + break; + case KEY_K: + validChars[i][0] = 'K'; + validChars[i][1] = 'k'; + break; + case KEY_L: + validChars[i][0] = 'L'; + validChars[i][1] = 'l'; + break; + case KEY_M: + validChars[i][0] = 'M'; + validChars[i][1] = 'm'; + break; + case KEY_N: + validChars[i][0] = 'N'; + validChars[i][1] = 'n'; + break; + case KEY_O: + validChars[i][0] = 'O'; + validChars[i][1] = 'o'; + break; + case KEY_P: + validChars[i][0] = 'P'; + validChars[i][1] = 'p'; + break; + case KEY_Q: + validChars[i][0] = 'Q'; + validChars[i][1] = 'q'; + break; + case KEY_R: + validChars[i][0] = 'R'; + validChars[i][1] = 'r'; + break; + case KEY_S: + validChars[i][0] = 'S'; + validChars[i][1] = 's'; + break; + case KEY_T: + validChars[i][0] = 'T'; + validChars[i][1] = 't'; + break; + case KEY_U: + validChars[i][0] = 'U'; + validChars[i][1] = 'u'; + break; + case KEY_V: + validChars[i][0] = 'V'; + validChars[i][1] = 'v'; + break; + case KEY_W: + validChars[i][0] = 'W'; + validChars[i][1] = 'w'; + break; + case KEY_X: + validChars[i][0] = 'X'; + validChars[i][1] = 'x'; + break; + case KEY_Y: + validChars[i][0] = 'Y'; + validChars[i][1] = 'y'; + break; + case KEY_Z: + validChars[i][0] = 'Z'; + validChars[i][1] = 'z'; + break; + case KEY_MINUS: + validChars[i][0] = '_'; + validChars[i][1] = '-'; + break; + default: + validChars[i][0] = 0; + validChars[i][1] = 1; + break; + } + } + + /* + * We need this command for when we trigger the app from the command line, + * because otherwise the upstroke on the enter key will kill the app. + */ + int waitEnter = 0; + char* ARGS[MAX_ARGS]; + int numArgs = 0; + ARGS[numArgs++] = "play"; + //ARGS[numArgs++] = "--directory=/home/markil3/.local/share/lollypop"; + char* BUFFER = (char*) calloc(ARG_LENGTH, sizeof(char)); + int index = 0; + BUFFER[index] = '\0'; + int fd; + int len; + if(argc < 2) { + printf("usage: %s \n", argv[0]); + callErrorSound(1); + return 1; + } + + fd = open(argv[1], O_RDONLY); + + if (fd < 0) + { + callErrorSound(2); + exit(2); + } + struct input_event ev; + + callSound("triggerActivate.wav"); + + while (len >= 0 && ev.value != 0 || waitEnter == 0 || (ev.code != KEY_ENTER && ev.code != KEY_ESC)) + { + len = read(fd, &ev, sizeof(struct input_event)); + + if (len < 0) + { + break; + } + + if ((ev.code == KEY_ENTER || ev.code == KEY_ESC) && ev.value == 1) + { + waitEnter = 1; + } + if (ev.type == 1 && ev.value == 1) + { + //printf("Keycode: %i, status %i\n", ev.code, ev.value); + if ((ev.code == KEY_SPACE || ev.code == KEY_ENTER) && index > 0) + { + if (ev.code == KEY_SPACE) + { + callSound("bit.wav"); + } + BUFFER[index] = '\0'; + ARGS[numArgs++] = BUFFER; + BUFFER = (char*) calloc(ARG_LENGTH, sizeof(char)); + index = 0; + } + else if (ev.code < NUM_KEYS && validChars[ev.code][1] != 0) + { + if (index < ARG_LENGTH) + { + BUFFER[index++] = validChars[ev.code][1]; + BUFFER[index] = '\0'; + callSound("bit.wav"); + } + } + } + } + + close(fd); + ARGS[numArgs] = NULL; + + if (len < 0) + { + callErrorSound(3); + exit(3); + } + if (index > 0) + { + /* + * If index was not reset, then we canceled. + */ + callSound("end.wav"); + exit(0); + } + + callSound("triggerDeactivate.wav"); + + printf("Running args: "); + for (int i = 0; i <= numArgs; i++) + { + printf("'%s' ", ARGS[i]); + } + printf("\n"); + + execvp(ARGS[0], ARGS); +} diff --git a/play b/play new file mode 100644 index 0000000..e3399ab --- /dev/null +++ b/play @@ -0,0 +1,272 @@ +#!/usr/bin/env python +import pathlib +from pathlib import Path +from urllib import parse +import subprocess +import sys +import random +from os.path import exists +import sqlite3 + +''' +A sorting method for songs. It works by pushing file names with more matches up higher. +''' +def getValueFunction(terms): + def valOfResult(song): + val = 0 + for arg in terms: + for part in song[2:]: + if arg in part.lower(): + val -= 1 + # Double every point received by a playlist + if song[0] == True: + val -= 1 + #print("%s got %d hits" % (song, val)) + return val + return valOfResult + +''' +Merges certain words into one. This is helpful for voice commands +''' +def mergeWords(argList): + i = 0 + quoting = False + while i < len(argList): + # Merge underscore for handling later + if quoting: + if argList[i] != "unquote": + argList[i - 1] = argList[i - 1] + " " + argList[i] + else: + quoting = False + del argList[i] + i -= 1 + elif argList[i] == "quote": + quoting = True + del argList[i] + elif i < len(argList) - 1 and argList[i] == "under" and argList[i + 1] == "score": + argList[i] = "underscore" + del argList[i + 1] + elif i < len(argList) - 1 and argList[i] == "play" and argList[i + 1] == "list": + argList[i] = "playlist" + del argList[i + 1] + if argList[i] == "dot": + argList[i - 1] = argList[i - 1] + "." + del argList[i] + i -= 1 + elif argList[i] == "underscore": + argList[i - 1] = argList[i - 1] + "_" + del argList[i] + i -= 1 + elif i < len(argList) - 1 and argList[i] == "dash": + argList[i] = "-" + argList[i + 1] + del argList[i + 1] + elif argList[i - 1].endswith(".") or argList[i - 1].endswith("_"): + argList[i - 1] = argList[i - 1] + argList[i] + del argList[i] + i -= 1 + i += 1 + + return argList + +def extractOptions(args): + options = {} + i = 0 + while i < len(args): + # Some dashed fields have special meanings. + if args[i].startswith("-") and args[i] != "-or" and args[i] != "-and" and args[i] != "--or" and args[i] != "--and": + if args[i].startswith("--"): + if "=" in args[i]: + options[args[i][2:args[i].index("=")]] = args[i][args[i].index("=") + 1:] + else: + options[args[i][2:]] = True + # These two have special meanings + else: + for char in args[i][1:]: + options[char] = True + del args[i] + elif args[i] == "playlist" or args[i] == "m3u": + options["playlist"] = True + del args[i] + elif args[i].endswith(".m3u"): + options["playlist"] = True + args[i] = args[i][0:args[i].rindex(".m3u")] + i += 1 + else: + i += 1 + return options + +def searchFiles(terms, options): + musicHome = Path(Path.home(), "Music") + if not musicHome.is_dir(): + # We are probably on Windows + musicHome = Path(Path.home(), "My Music") + + findArgs = ["find", str(musicHome)] + terms = appendSearchArguments(findArgs, sys.argv[1:], options) + print(" ".join(findArgs)) + print() + process = subprocess.Popen(findArgs, stdout=subprocess.PIPE) + output, error = process.communicate() + files = output.decode().split("\n") + # Remove any empty things + while "" in files: + files.remove("") + return files + +''' +Compiles all arguments in the argument list into a SQLite search term + +@return A set of search terms provided, stripped of all extra arguments. +''' +def appendDatabaseArguments(argList, args, options, playlist): + notArgs = [] + terms = set() + if len(args) > 0: + for arg in args: + if arg.startswith("!"): + notArgs.append(arg[1:]) + elif arg == "AND" or arg == "and" or arg == "-and" or arg =="--and" or arg == "&&": + # Override whatever AND/OR we had before + argList.pop() + argList.append("AND") + elif arg == "OR" or arg == "or" or arg == "-or" or arg =="--or" or arg == "||": + # Override whatever AND/OR we had before + argList.pop() + argList.append("OR") + else: + argList.append("(") + if playlist: + argList.append("playlists.name") + argList.append("LIKE") + argList.append("'%" + arg + "%'") + else: + argList.append("tracks.name") + argList.append("LIKE") + argList.append("'%" + arg + "%'") + argList.append("OR") + argList.append("albums.name") + argList.append("LIKE") + argList.append("'%" + arg + "%'") + argList.append("OR") + argList.append("artists.name") + argList.append("LIKE") + argList.append("'%" + arg + "%'") + argList.append(")") + terms.add(arg) + if "a" in options: + argList.append("AND") + else: + argList.append("OR") + if argList[len(argList) - 1] == "AND" or argList[len(argList) - 1] == "OR": + argList.pop() + if len(notArgs) > 0: + argList.append("AND") + for arg in notArgs: + argList.append("tracks.name") + argList.append("NOT") + argList.append("LIKE") + argList.append("'%" + arg + "%'") + argList.append("AND") + argList.pop() + return terms + +def searchDatabase(options): + files = [] + + if "directory" in options: + dbDir = Path(options["directory"]) + else: + dbDir = Path(Path.home(), ".local/share/lollypop") + + if "playlist" in options: + db = sqlite3.connect(str(Path(dbDir, "playlists.db"))) + findArgs = "SELECT tracks.uri, playlists.name FROM tracks INNER JOIN playlists ON tracks.playlist_id = playlists.id WHERE".split(" ") + terms = appendDatabaseArguments(findArgs, sys.argv[1:], options, True) + findArgs = " ".join(findArgs) + ";" + results = db.execute(findArgs); + for row in results: + files.append((True,) + row) + db.close() + db = sqlite3.connect(str(Path(dbDir, "lollypop.db"))) + findArgs = "SELECT tracks.uri, tracks.name, albums.name, artists.name FROM tracks INNER JOIN albums ON tracks.album_id = albums.id INNER JOIN track_artists ON tracks.id = track_artists.track_id INNER JOIN artists ON track_artists.artist_id = artists.id WHERE".split(" ") + terms = appendDatabaseArguments(findArgs, sys.argv[1:], options, False) + findArgs = " ".join(findArgs) + ";" + results = db.execute(findArgs); + for row in results: + files.append((False,) + row) + db.close() + + return (files, terms) + +if __name__ == "__main__": + # Checks if VLC is installed + missing = subprocess.call("command -v vlc >> /dev/null", shell=True) + if missing: + print("VLC Media Player is not installed", file=sys.stderr) + exit(1) + + print(sys.argv) + mergeWords(sys.argv) + options = extractOptions(sys.argv) + if "h" in options or "help" in options: + print("""Usage: %s [OPTIONS]... [TERM]... +Searches for a song within the music collection + +OPTIONS: +--directory=[dir] Define the database directory. +-a The search must match every single term +-v Displays the media player +-l Loops through the found files until a new command is entered. +-s Only the first option will play. +-h, --help Shows this message + +TERMS: +For the most part, you can just enter a single-word term, and it will be added +to the possible substrings included in the file name search. However, there are +a few additional ways to refine your search. + +-and, --and, AND, and Any of these keywords will cause the previous and next + terms to be linked together, so that they MUST appear + for the file to qualify. +-or, --or, OR, or Any of these keywords will cause the previous and next + terms to be linked together, so that only one or the + other needs to appear for the file to qualify. + Overrides the -a option for those two terms. +! Add this to the beginning of a term to ensure that the + keyword does NOT appear in any matching file paths. +m3u, playlist Normally, playlists are ignored during the file search. + However, this term will not only allow for their + search, but will also give them higher precedence.""" % (sys.argv[0])) + exit(0) + + files, terms = searchDatabase(options) + + if len(files) == 0: + print("No files found", file=sys.stderr) + exit(1) + + random.shuffle(files) + files.sort(key=getValueFunction(terms)) + for i in range(len(files)): + files[i] = files[i][1] + if "s" in options: + files = [files[0]] + for song in files: + print(song) + + if "v" in options: + vlcArgs = ["vlc"] + else: + vlcArgs = ["cvlc"] + if "l" in options: + vlcArgs.append("--loop") + else: + files.append("vlc://quit") + vlcArgs += files + #print(" ".join(vlcArgs)) + print("Launching VLC: %s" % (vlcArgs)) + process = subprocess.Popen(vlcArgs, stdout=sys.stdout, stderr=sys.stderr) + if not "l" in options: + exit(process.wait()) + else: + exit(0)