diff --git a/doubly_linked_list.py b/LinkedLists/doubly_linked_list.py similarity index 100% rename from doubly_linked_list.py rename to LinkedLists/doubly_linked_list.py diff --git a/singly_linked_list.py b/LinkedLists/singly_linked_list.py similarity index 100% rename from singly_linked_list.py rename to LinkedLists/singly_linked_list.py diff --git a/SearchPlayer/Dockerfile b/SearchPlayer/Dockerfile new file mode 100644 index 0000000..483b441 --- /dev/null +++ b/SearchPlayer/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.7.4-alpine3.10 + +ADD requirements.txt /app/requirements.txt + +RUN set -ex \ + && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ + && python -m venv /env \ + && /env/bin/pip install --upgrade pip \ + && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ + && runDeps="$(scanelf --needed --nobanner --recursive /env \ + | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ + | sort -u \ + | xargs -r apk info --installed \ + | sort -u)" \ + && apk add --virtual rundeps $runDeps \ + && apk del .build-deps + +ADD . /app +WORKDIR /app + +ENV VIRTUAL_ENV /env +ENV PATH /env/bin:$PATH + +EXPOSE 8000 + +CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "SearchPlayer.wsgi:application"] diff --git a/SearchPlayer/README.md b/SearchPlayer/README.md new file mode 100755 index 0000000..415e8a0 --- /dev/null +++ b/SearchPlayer/README.md @@ -0,0 +1,66 @@ +# SearchPlayer + +The purpose of this project is to provide a "power user" kind of interface for a desktop music player. +The basic idea is that for power users, typing is faster than clicking. + +This project is still in development. At the moment it is usable, but glitches may occur. Specifically, the interface is not responsive and works mostly for resolutions above 1024x768. + +**DEPENDENCIES NOTE**: This project depends on the modules: + - `django-mathfilters` + - `glob2` + +**COMPATIBILITY NOTE**: To keep the player compatible cross-browser, the code will first check if the browser supports MP3 file playback. If it doesn't, it will look for a file with the same name, but extension '.ogg' + +## Usage + + + +A basic search will return results that include the search term in the artist name, album name or song title. +When more search terms are used, the results include *ANY* of the terms. +To filter the results using *ALL* of the terms, it's possible to combine multiple terms with a ":" (colon) separator. + +For instance: + - `meta` will return all matches from the word "meta" (e.g. "metallica") + - `meta mega` will return all matches from the words "meta" *OR* "mega" (e.g. "metallica" or "megadeth") + - `meta:one` will return all matches from the words "meta" *AND* "one" (e.g. song "one" by "metallica") + - `meta:one mega` will return all matches from the words "meta" *AND* "one" plus all the matches from the word "mega" (e.g. song "one" by "metallica" plus all songs by "megadeth") + +The standard search is done on a search key based on the combination of the three elements, so that looking for multiple words +in a single song can also be done by typing the title with no spaces (e.g. "blackice" will return the album "black ice"). + +The regex search is done applying the regular expression ONLY to the title. +This is not really for performance issues, but more for making it so that the query results are more intuitive. + +There are two reserved words in the search: + - "random" will pick a single result out of the current single query. This is done for a single search term. For instance: + - `random:meta` will return a random match for the word "meta" + - `random:meta:one` will return a random match for the word "meta" combined with the word "one" + - `random:meta:one mega` will return a random match for the word "meta" combined with the word "one" plus all the matches for the word "mega" + - "shuffle" will shuffle the results randomly for the current single query. By default, results are ordered by artist / album / track number. For instance: + - `shuffle:meta` will return all result for the word "meta", but they will be randomly shuffled. + - `shuffle:meta:one` will return all result for the word "meta:one", but they will be randomly shuffled. + - `shuffle:meta:one mega` will return all result for the word "meta:one", but they will be randomly shuffled plus all the matches for the word "mega" which will instead be ordered as default. + +The "random" and "shuffle" reserved words must be the first word in the current query. + +## Keyboard + +Pressing the enter key will either: + - start playback if a song is *NOT* already being played. + - add the results to the queue if a song is already being played. + - add the results to the top of the queue if the *SHIFT* button is also pressed. + - replace the current playlist with the current results if the *CTRL* button is also pressed. + +Pressing the arrow down button will move to the next track. +Pressing the arrow up button will move to the prev track. + +Pressing the Ctrl+X combination will enable regex mode. This will apply regular expressions to title, artist, album and show the results of those three matches combined. It is still possible to filter using the ":" (colon) separator as specified above. + +## Setup + +This project is done entirely in Python+Django and Javascript, so a working Python+Django environment is needed. +The database is a sqlite3 file, so it can be redistributed easily as a demo. + +The "file_indexer.py" script will insert in the database the titles and paths of the music files. +It can be configured so that it reads files from a specific directory, the default one is "Music/" from where the script is run. +It works by reading the MP3 files metadata, so if some fields are missing, check that the metadata in the MP3 files is correct. diff --git a/SearchPlayer/SearchPlayer/urls.py b/SearchPlayer/SearchPlayer/urls.py index 10b0d89..c184b5b 100755 --- a/SearchPlayer/SearchPlayer/urls.py +++ b/SearchPlayer/SearchPlayer/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^lister/', include('lister.urls')), + url(r'^$', include('lister.urls')), ] diff --git a/SearchPlayer/TODO.txt b/SearchPlayer/TODO.txt index 6c600b9..83e32c3 100755 --- a/SearchPlayer/TODO.txt +++ b/SearchPlayer/TODO.txt @@ -1,9 +1,5 @@ - Cleanup main.js -- Cleanup python code -- Fix sorting when mixing filters and search words -- Fix playing when not submitted (e.g. when clicking with the mouse before pressing enter) -- Add prefixes: - - "repeat:" adds results and repeats them. This replaces the current list -- Add history of recently played songs -- Implement callback to play through shown items instead of having the path saved in the template +- Add case sensitive search (maybe mapping Ctrl+M) +- Keep track of how many times each song has been played +- Add "suggest:" filter based on most played songs diff --git a/SearchPlayer/db.sqlite3 b/SearchPlayer/db.sqlite3 index 0c48a2e..83a8890 100644 Binary files a/SearchPlayer/db.sqlite3 and b/SearchPlayer/db.sqlite3 differ diff --git a/SearchPlayer/docker-compose.yml b/SearchPlayer/docker-compose.yml new file mode 100644 index 0000000..70b015b --- /dev/null +++ b/SearchPlayer/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.7' + +services: + web: + build: . + command: gunicorn SearchPlayer.wsgi:application --bind 0.0.0.0:8000 + expose: + - 8000 + volumes: + - ./lister/static:/opt/app/searchplayer/lister/static + + nginx: + image: nginx:latest + container_name: nginx_01 + ports: + - "8000:8000" + volumes: + - ./:/app # for syncing with django source code + - ./nginx:/etc/nginx/conf.d + depends_on: + - web diff --git a/SearchPlayer/file_indexer.py b/SearchPlayer/file_indexer.py index 2ef16c5..6422cbb 100755 --- a/SearchPlayer/file_indexer.py +++ b/SearchPlayer/file_indexer.py @@ -28,7 +28,7 @@ def update_db(): artist_id += 1 artist_lookup[song.tags['ARTIST'][0]] = artist_id artists.append((artist_id, song.tags['ARTIST'][0])) - songs.append((album_id, artist_id, os.path.dirname(os.path.realpath(file)) + '/Folder.jpg', + songs.append((album_id, artist_id, os.path.dirname(os.path.realpath(file)) + '/Cover.png', song.tags['DATE'][0], 0, song.tags['TITLE'][0], song.tags['TRACKNUMBER'][0], file)) cursor.execute('DELETE FROM lister_song') diff --git a/SearchPlayer/lister/static/Music/Cover.png b/SearchPlayer/lister/static/Music/Cover.png new file mode 100644 index 0000000..74cd12b Binary files /dev/null and b/SearchPlayer/lister/static/Music/Cover.png differ diff --git a/SearchPlayer/lister/static/Music/demo.mp3 b/SearchPlayer/lister/static/Music/demo.mp3 new file mode 100644 index 0000000..64a6583 Binary files /dev/null and b/SearchPlayer/lister/static/Music/demo.mp3 differ diff --git a/SearchPlayer/lister/static/Music/demo.ogg b/SearchPlayer/lister/static/Music/demo.ogg new file mode 100644 index 0000000..c002927 Binary files /dev/null and b/SearchPlayer/lister/static/Music/demo.ogg differ diff --git a/SearchPlayer/lister/static/lister/main.css b/SearchPlayer/lister/static/lister/main.css index 60cf8be..6d391e8 100755 --- a/SearchPlayer/lister/static/lister/main.css +++ b/SearchPlayer/lister/static/lister/main.css @@ -12,12 +12,14 @@ body { -ms-text-overflow: ellipsis; } + /* Main content styling */ .wrapper-content { position: relative; + display: inline; } #container-frame { @@ -38,7 +40,7 @@ body { border-radius: 19px; } -.standard-header > li { +.standard-header>li { display: inline-block; color: #444; font-weight: bold; @@ -71,11 +73,11 @@ body { display: none; } -.main-content > ul { +.main-content>ul { border-radius: 35px; } -.main-content > ul:hover { +.main-content>ul:hover { background-color: #BBCCCC; font-weight: bold; } @@ -85,7 +87,7 @@ body { font-weight: bold; } -.main-content > ul > li { +.main-content>ul>li { display: inline-block; /* width: 18%; */ padding: 10px 5px; @@ -99,7 +101,7 @@ body { -ms-text-overflow: ellipsis; } -.player-controls > div { +.player-controls>div { display: inline-block; margin-top: 5px; } @@ -192,13 +194,13 @@ body { width: 20%; } -#current-track > img { +#current-track>img { width: 64px; height: 64px; border-radius: 15px; } -#current-track > #current-title { +#current-track>#current-title { position: absolute; margin-top: 5px; margin-left: 5px; @@ -206,7 +208,7 @@ body { width: 72%; } -#current-track > #current-artist { +#current-track>#current-artist { position: absolute; top: 60%; margin-left: 5px; @@ -234,6 +236,26 @@ a { color: #444; } +.regex-hint-container { + margin-left: 20px; +} + +.regex-hint { + font-size: 13px; + color: #7d7d7d; +} + +#regex-status { + font-size: 33px; + position: absolute; + left: 94%; + top: 16px; + border-radius: 35px; + border: 5px solid #888; + color: #7d7d7d; +} + + /* Search box styling */ @@ -249,85 +271,89 @@ a { -webkit-padding-start: 0; margin: 0 auto; } - + .mainsearch-form { overflow: hidden; position: relative; } - + .mainsearch-input-wrapper { padding: 0 66px 0 0; overflow: hidden; } .mainsearch-input { - width: 100%; + width: 93%; } + /*********************** * Configurable Styles * ***********************/ + .mainsearch { - padding: 0 0px 0 0px; /* Padding for other horizontal elements */ + padding: 0 0px 0 0px; + display: inline; } .mainsearch-input { - -webkit-box-sizing: content-box; + -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; height: 40px; - padding: 0 46px 0 10px; + padding: 0 46px 0 10px; border-color: #888; - border-radius: 35px; /* (height/2) + border-width */ - border-style: solid; + border-radius: 35px; + /* (height/2) + border-width */ + border-style: solid; border-width: 5px; - margin-top: 15px; - color: #333; - font-family: 'Enriqueta', arial; + margin-top: 15px; + color: #333; + font-family: 'Enriqueta', arial; font-size: 26px; -webkit-appearance: none; -moz-appearance: none; } - + .mainsearch-submit { - position: absolute; - right: 0; + position: absolute; + right: 6%; top: 0; display: block; width: 60px; height: 60px; - padding: 0; - border: none; + padding: 0; + border: none; margin-top: 9px; - margin-right: 5px; + margin-right: 5px; background: transparent; - color: #888; - font-family: 'Enriqueta', arial; - font-size: 40px; - line-height: 60px; - -webkit-transform: rotate(-45deg); - -moz-transform: rotate(-45deg); + color: #888; + font-family: 'Enriqueta', arial; + font-size: 40px; + line-height: 60px; + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); -o-transform: rotate(-45deg); } .mainsearch-input:focus { - outline: none; - border-color: #333; + outline: none; + border-color: #333; } .mainsearch-input:focus.mainsearch-submit { - color: #333; + color: #333; } .mainsearch-submit:hover { - color: #333; - cursor: pointer; + color: #333; + cursor: pointer; } ::-webkit-input-placeholder { - color: #888; + color: #888; } input:-moz-placeholder { - color: #888 -} + color: #888 +} \ No newline at end of file diff --git a/SearchPlayer/lister/static/lister/main.js b/SearchPlayer/lister/static/lister/main.js index 37ed6cd..6ab0186 100755 --- a/SearchPlayer/lister/static/lister/main.js +++ b/SearchPlayer/lister/static/lister/main.js @@ -10,6 +10,8 @@ var currentTitle = $('#current-title'); var currentArtist = $('#current-artist'); var finderBox = $('.mainsearch'); var inputBox = $('.mainsearch-input'); +var regexStatus = $('#regex-status'); +var regexEnabled = false; var trackList = []; var searchResults = []; var shuffledList = []; @@ -22,6 +24,7 @@ var playListTemplate; var prevSearchTerm; var currentSearchTerm; var displayedSongIDs = {}; +var mustRefresh = false; var trackListOperations = { REPLACE: 1, @@ -46,23 +49,25 @@ function clickPercent(e) { function showSongs(searchTerm) { songs = []; - if (currentSearchTerm !== searchTerm) { + if ((currentSearchTerm !== searchTerm) || (mustRefresh)) { + mustRefresh = false; currentSearchTerm = searchTerm; $.get('/lister/songs/', function(template) { playListTemplate = template; if (searchTerm === '') { - var html = Mustache.render(playListTemplate, {"search_results": [], "playlist": trackList}); + var html = Mustache.render(playListTemplate, { "search_results": [], "playlist": trackList }); $('#container-frame').html(html); - $('#search-results').css({display: 'block'}); - $('#playlist').css({display: 'none'}); + $('#search-results').css({ display: 'block' }); + $('#playlist').css({ display: 'none' }); return; } - $.getJSON('/lister/search/' + searchTerm, function(songs) { + var getUrl = regexEnabled ? '/lister/search/regex/' : '/lister/search/'; + $.getJSON(getUrl + searchTerm, function(songs) { searchResults = songs.songs_list; - var html = Mustache.render(playListTemplate, {"search_results": searchResults, "playlist": trackList}); + var html = Mustache.render(playListTemplate, { "search_results": searchResults, "playlist": trackList }); $('#container-frame').html(html); - $('#search-results').css({display: 'block'}); - $('#playlist').css({display: 'none'}); + $('#search-results').css({ display: 'block' }); + $('#playlist').css({ display: 'none' }); }); }); } @@ -82,6 +87,9 @@ function playTrack(track) { currentTitle.html(trackList[track].title); currentArtist.html(trackList[track].artist); playerElement.src = trackList[track].path; + if (playerElement.canPlayType('audio/mpeg') === '') { + playerElement.src = trackList[track].path.substr(0, trackList[track].path.lastIndexOf('.')) + '.ogg'; + } playerElement.load(); playerElement.play(); playButton.removeClass('play-button'); @@ -90,10 +98,10 @@ function playTrack(track) { } function getCoverPathFromSongPath(songPath) { - return songPath.substring(0, songPath.lastIndexOf('/')) + '/Folder.jpg'; + return songPath.substring(0, songPath.lastIndexOf('/')) + '/Cover.png'; } -function setCurrentTrackList(searchTerm, searchResults, operation = trackListOperations.APPEND ) { +function setCurrentTrackList(searchTerm, searchResults, operation = trackListOperations.APPEND) { prevSearchTerm = searchTerm; if (operation === trackListOperations.REPLACE) { trackList = searchResults; @@ -118,10 +126,17 @@ function setCurrentTrackList(searchTerm, searchResults, operation = trackListOpe trackList = trackList.concat(filteredSearchResults); } //TODO: This can be optimized to only render the new data and append it - var html = Mustache.render(playListTemplate, {"playlist": trackList}); + var html = Mustache.render(playListTemplate, { "playlist": trackList }); $('#container-frame').html(html); - $('#search-results').css({display: 'none'}); - $('#playlist').css({display: 'block'}); + updateDisplaySelectedTrack(); +} + +function updateDisplaySelectedTrack() { + $('#search-results').css({ display: 'none' }); + $('#playlist').css({ display: 'block' }); + if (!playerElement.paused || playerElement.currentTime) { + $('[data-index-playlist=' + trackList[currentTrack].song_id + ']').closest('ul').addClass('active-track'); + } } function timeUpdate() { @@ -130,7 +145,7 @@ function timeUpdate() { timeLineHead.css('left', playPercent + 'px'); } -window.addEventListener('load', function() {showSongs('')}, false); +window.addEventListener('load', function() { showSongs('') }, false); window.addEventListener('resize', function() { timelineWidth = $('#timeline-container').outerWidth() - 20; //Need to compensate for head size }); @@ -139,18 +154,39 @@ window.addEventListener('submit', function(event) { event.preventDefault(); }); +function toggleRegex() { + if (regexEnabled) { + regexStatus.css('border-color', '#7D7D7D'); + regexStatus.css('color', '#7D7D7D'); + regexEnabled = false; + } else { + regexStatus.css('border-color', '#FF8800'); + regexStatus.css('color', '#FF8800'); + regexEnabled = true; + } + mustRefresh = true; +} + function bindUI() { playerElement.load(); finderBox.bind('keyup', function(event) { - if (event.key != "Enter") { + if (event.key === "ArrowDown") { + playTrack(currentTrack + 1); + return; + } + if (event.key === "ArrowUp") { + playTrack(currentTrack - 1); + return; + } + if ((event.key === 'x') && (event.ctrlKey)) { + toggleRegex(); + } + if ((event.key != "Enter") || (mustRefresh)) { if (inputBox.val() !== '') { showSongs(inputBox.val()); } else { - $('#search-results').css({display: 'none'}); - $('#playlist').css({display: 'block'}); - if (!playerElement.paused || playerElement.currentTime) { - $('[data-index-playlist=' + trackList[currentTrack].song_id + ']').closest('ul').addClass('active-track'); - } + mustRefresh = false; + updateDisplaySelectedTrack(); } } else { if (searchResults.length > 0) { @@ -230,4 +266,4 @@ function bindUI() { }); } -$(document).ready(bindUI); +$(document).ready(bindUI); \ No newline at end of file diff --git a/SearchPlayer/lister/templates/lister/wrapper.html b/SearchPlayer/lister/templates/lister/wrapper.html index a9deb3e..e3c9071 100644 --- a/SearchPlayer/lister/templates/lister/wrapper.html +++ b/SearchPlayer/lister/templates/lister/wrapper.html @@ -13,14 +13,15 @@