diff --git a/BattleGame/README.md b/BattleGame/README.md deleted file mode 100755 index 3fabfa3..0000000 --- a/BattleGame/README.md +++ /dev/null @@ -1,50 +0,0 @@ -### General description -This is a very simple text game to make practice with Python classes. -The basic idea of the game is to have a character (the "player") move from -battle to battle, with breaks in-between to visit a shop that allows to buy -a single random wearable item and some potions. -It is also possible for the player to trigger a "hard battle" by sacrificing -enough "illbane" to summon a stronger enemy. -The player can obtain "illbane" by eniemes that drop it when defeated, or -save money to buy it in the shop. -The goal of the game is to survive as long as possible to get the highest score. -The original game was a simple Java implementation for which someone asked a -review on stackexchange. I started the review, but I noticed that I had almost -entirely re-written the code. I decided to use this as an opportunity to improve -my Python knowledge. - -### Functional specifications -- The player starts with 100 HP, 30 strength, 20 as the maximum random damage, -1000 gold and an health potion. -- Initially the player can: - - Start a fight. This triggers the battle choices menu. The enemy will be randomly chosen. - - Visit the shop. This triggers the shop choices menu. Both items and potions -are available. - - Sacrifice illbane. This triggers a hard battle. The player needs to have -enough illbane for this to happen. - -### Battle system -- During the fight the player can: - - Attack the enemy. This means that the enemy will also attack the player. -The player attack damage is subtracted from the enemy's health and the enemy's -attack damage is subtracted from the player's health. When either reaches zero, -the fight is over. If the player's health reaches zero, the game is also over. -If the enemy is defeated and the player is still alive, the enemy will drop a -random amount of gold and a single unit of illbane. There is a random chance -that the enemy will also drop an health potion. The score will also be updated -based on the enemy value. - - Drink a potion. Currently two kinds of potions are supported: - - Health potion. This restores part of the player's health. - - Strength potion. This increases the player's attack allowing to do more damage. - - Run from the battle. This ends the current battle and sends the player back to - the main actions choice. - -### Shop system -- In the shop the player can buy either an item or a potion. -- Every item has a specific cost, armor bonus and strength bonus. These are added -to the player stats when they're bought. -- Every potion has a specific value for how much health it will recover, or how much -strength it will increase. The effect of strength potion is canceled when a fight is -terminated, either by winning or by running away. -- Internally, to determine if the bonus should be added immediately or only on consumption, -the item has a "wearable" attribute that determines the kind of item. diff --git a/BattleGame/boss.py b/BattleGame/boss.py deleted file mode 100755 index 8df1cd0..0000000 --- a/BattleGame/boss.py +++ /dev/null @@ -1,19 +0,0 @@ -from character import Character -import random - - -class Boss(Character): - special_attack_chance = 0 - - def do_special_attack(self): - print(self.name + " launches its special attack!") - return random.randint(self.strength, self.strength + self.max_random_damage) - - def do_attack(self): - self.special_attack_chance += 100 - \ - (self.health / self.base_health) * 100 - chance = random.randint(0, 100) - if (chance < self.special_attack_chance): - return self.do_special_attack() - else: - return random.randint(0, self.max_random_damage) diff --git a/BattleGame/character.py b/BattleGame/character.py deleted file mode 100755 index b7b2b76..0000000 --- a/BattleGame/character.py +++ /dev/null @@ -1,30 +0,0 @@ -import random - - -class Character: - - def __init__(self, name, base_health, base_strength, max_random_damage, gold, inventory): - self.name = name - self.base_health = base_health - self.health = base_health - self.base_strength = base_strength - self.strength = base_strength - self.inventory = inventory - self.max_random_damage = max_random_damage - self.gold = gold - self.armor = 0 - self.illbane = 1 - - def is_alive(self): - if self.health > 1: - return True - return False - - def do_attack(self): - return random.randint(0, self.max_random_damage) - - def get_dropped_object(self): - chance = random.randint(0, 2) - if (chance >= 1): - return Potion("Health Potion", 100, 30, 1, 50) - return None diff --git a/BattleGame/item.py b/BattleGame/item.py deleted file mode 100755 index 4c5f721..0000000 --- a/BattleGame/item.py +++ /dev/null @@ -1,10 +0,0 @@ -class Item: - - def __init__(self, name, cost, health_bonus, strength_bonus, wearable, illbane): - self.name = name - self.cost = cost - self.health_bonus = health_bonus - self.strength_bonus = strength_bonus - self.illbane = illbane - self.soldOut = False - self.is_wearable = wearable diff --git a/BattleGame/main.py b/BattleGame/main.py deleted file mode 100755 index 570d9a3..0000000 --- a/BattleGame/main.py +++ /dev/null @@ -1,108 +0,0 @@ -# Check difference between randint and randrange -# Move "common_enemies" variable so it's not global - -import random -from boss import Boss -from character import Character -from player import Player -from item import Item -from shop import Shop -import utils - -bosses = [] -common_enemies = [] -shop_items = [] -potion_types = [] -all_potions = [] - - -def start_battle(player, enemy=None): - global common_enemies, all_potions - - if (enemy == None): - enemy = common_enemies[random.randint(0, len(common_enemies) - 1)] - - print("\t# " + enemy.name + " appears! #\n") - - fighting = True - - while (fighting and enemy.is_alive() and player.is_alive()): - utils.display_status(player, enemy) - battle_choice = utils.get_battle_choice(player, enemy) - if (battle_choice == 1): - damage_dealt = random.randint(0, player.strength - 1) - damage_taken = enemy.do_attack() - enemy.health -= damage_dealt - player.health -= damage_taken - print("\t> You strike the " + enemy.name + - " for " + str(damage_dealt) + " damage.") - print("\t> You receive " + str(damage_taken) + " in retaliation!") - if (battle_choice == 2): - potion = utils.get_potion_choice(all_potions) - if (potion in player.inventory): - player.drink(potion) - else: - print("\tYou don't have any " + potion.name + "!") - if (battle_choice == 3): - print("------") - print("\tYou run away from the " + enemy.name + "!") - print("------") - fighting = False - - if (not player.is_alive()): - print("\n****** You crawl out of the dungeon to live and fight another day. ******\n\n") - return - - if (not enemy.is_alive()): - dropped_illbane = random.randint(1, 3) - player.illbane += dropped_illbane - print("------") - print("\t" + enemy.name + " has been defeated!") - print("\tYou find " + str(enemy.gold) + - " gold on the " + enemy.name + "!") - print("\tYou collect " + str(dropped_illbane) + - " illbane from " + enemy.name + "!") - player.gold += enemy.gold - item = enemy.get_dropped_object() - if item != None: - print("\t" + enemy.name + " has dropped " + item.name + "!") - player.inventory.append(item) - print("------") - - -def sacrifice_illbane(player): - global bosses - - if player.illbane >= 4: - player.illbane -= 4 - start_battle(player, bosses[random.randint(0, len(bosses) - 1)]) - else: - print("\nYou do not have enough illbane!\n") - - -def run_game(): - global bosses, common_enemies, shop_items, all_potions - - common_enemies = utils.init_common_enemies() - bosses = utils.init_bosses() - all_potions = utils.get_all_potions() - shop = Shop(all_potions) - player = Player("The player", 100, 30, 20, 1000, - [all_potions[0]]) - player.illbane = 1 - - must_quit = False - while player.is_alive() and not must_quit: - idle_choice = utils.get_idle_choice(player) - if (idle_choice == 1): - start_battle(player) - if (idle_choice == 2): - shop.visit(player) - if (idle_choice == 3): - sacrifice_illbane(player) - if (idle_choice == 4): - must_quit = True - - -if __name__ == "__main__": - run_game() diff --git a/BattleGame/player.py b/BattleGame/player.py deleted file mode 100755 index 991afe5..0000000 --- a/BattleGame/player.py +++ /dev/null @@ -1,14 +0,0 @@ -from character import Character -from potion import Potion -import random - - -class Player(Character): - - def drink(self, potion): - self.strength *= potion.strength_bonus - self.health += potion.health_bonus - if (self.health > self.base_health): - self.health = self.base_health - self.inventory.remove(potion) - print("You drink a " + potion.name + "!") diff --git a/BattleGame/shop.py b/BattleGame/shop.py deleted file mode 100755 index f74d92f..0000000 --- a/BattleGame/shop.py +++ /dev/null @@ -1,79 +0,0 @@ -import utils -from item import Item - - -class Shop: - - def __init__(self, potions): - self.items = [ - Item("Silver Sword", 1000, 0, 100, True, 0), - Item("Steel Sword", 250, 0, 25, True, 0), - Item("Iron Helmet", 150, 10, 0, True, 0), - Item("Iron Chestplate", 200, 18, 0, True, 0), - Item("Iron Boots", 100, 8, 0, True, 0), - Item("Iron Gauntlets", 75, 5, 0, True, 0), - Item("Steel Helmet", 400, 5, 0, True, 0), - Item("Steel Chestplate", 600, 10, 10, True, 0), - Item("Steel Boots", 300, 10, 0, True, 0), - Item("Steel Gauntlets", 250, 7, 0, True, 0), - Item("Illbane", 2500, 0, 0, False, 1), - ] - - self.potions = potions - - def get_item_choice(self, player): - all_items = self.items + self.potions - - utils.display_status(player) - print('What would you like to buy?') - print('{s:{c}^{n}}'.format(s='', n=54, c='#')) - print(" {:20}| {:>8}|{:>8}|{:>8}|".format( - 'Item', 'Health', 'Strength', 'Cost')) - index = 0 - for shopitem in all_items: - index += 1 - print(' {:2}. {:20}| {:>8}|{:>8}|{:>8}|'.format( - index, shopitem.name, shopitem.health_bonus, shopitem.strength_bonus, shopitem.cost)) - print(' ' + str(index + 1) + '. Exit shop') - print('{s:{c}^{n}}'.format(s='', n=54, c='#')) - - user_choice = utils.get_valid_input(index + 1) - if user_choice <= len(all_items): - return all_items[user_choice - 1] - - return None - - def visit(self, player): - staying_in_shop = True - staying_in_potion_shop = True - - while (staying_in_shop): - chosen_item = self.get_item_choice(player) - if (chosen_item == None): - staying_in_shop = False - else: - if (chosen_item.is_wearable) and (chosen_item in player.inventory): - print("You already have " + chosen_item.name + "!") - continue - - if (chosen_item.is_wearable): - if (player.gold >= chosen_item.cost): - player.gold -= chosen_item.cost - player.illbane += chosen_item.illbane - player.base_strength += chosen_item.strength_bonus - player.base_health += chosen_item.health_bonus / 2 - player.inventory.append(chosen_item) - else: - print("You don't have enough gold!") - else: - print('How many ' + chosen_item.name + '?') - amount = utils.get_valid_input(10) - if (player.gold >= amount * chosen_item.cost): - staying_in_shop = False - print("You got " + chosen_item.name + "!") - player.gold -= amount * chosen_item.cost - for i in range(amount): - player.inventory.append(chosen_item) - else: - print("You don't have enough gold!") - print("Goodbye") diff --git a/BattleGame/utils.py b/BattleGame/utils.py deleted file mode 100755 index 0cf80e6..0000000 --- a/BattleGame/utils.py +++ /dev/null @@ -1,118 +0,0 @@ -import random -from boss import Boss -from character import Character -from player import Player -from item import Item - - -def init_bosses(): - bosses_list = [ - Boss("White Dragon", random.randint(150, 250), - 40, 15, random.randint(0, 10000), []), - Boss("Blue Dragon", random.randint(160, 230), - 45, 25, random.randint(0, 10000), []), - Boss("Red Dragon", random.randint(170, 220), - 50, 35, random.randint(0, 10000), []), - Boss("Black Dragon", random.randint(200, 250), - 60, 50, random.randint(0, 10000), []) - ] - - return bosses_list - - -def init_common_enemies(): - enemy_list = [ - Character("Kobold", random.randint(50, 150), - 25, 10, random.randint(0, 1000), []), - Character("Kobold Warrior", random.randint(70, 220), - 30, 20, random.randint(0, 1000), []), - Character("Kobold Archer", random.randint(90, 290), - 40, 30, random.randint(0, 1000), []), - Character("Kobold Overseer", random.randint( - 150, 400), 50, 40, random.randint(0, 1000), []) - ] - - return enemy_list - - -def get_valid_input(max_choice): - user_choice = raw_input() - while (user_choice == '') or ((user_choice < 1) and (user_choice > max_choice)): - print "Invalid choice\n" - user_choice = raw_input() - try: - user_choice_int = int(user_choice) - return user_choice_int - except Exception: - print('ValueError') - user_choice = '' - return int(user_choice) - - -def get_battle_choice(player, enemy): - print("####################") - print("What would you like to do?") - print(" 1. Attack") - print(" 2. Drink potion") - print(" 3. Run!") - print("####################") - - return get_valid_input(3) - - -def get_idle_choice(player): - display_status(player) - print(" 1. Fight") - print(" 2. Visit the shop") - print(" 3. Sacrifice Illbane...") - print(" 4. Exit dungeon") - print("####################") - - return get_valid_input(4) - - -def get_all_potions(): - return [ - Item("Health Potion", 100, 30, 1, False, 0), - Item("Strength Potion", 500, 0, 2, False, 0) - ] - - -def get_potion_choice(potions): - print("####################") - index = 0 - for potion in potions: - index += 1 - print(" " + str(index) + ". " + potion.name) - print("####################") - - return potions[get_valid_input(index) - 1] - - -def display_status(player, enemy=None): - if (enemy == None): - print('{s:{c}^{n}}'.format(s='', n=20, c='#')) - print('# {:>10}'.format('Health: ') + - '{:6} #'.format(str(player.health))) - print('# {:>10}'.format('Strength: ') + - '{:6} #'.format(str(player.strength))) - print('# {:>10}'.format('Illbane: ') + - '{:6} #'.format(str(player.illbane))) - print('# {:>10}'.format('Gold: ') + - '{:6} #'.format(str(player.gold))) - print('{s:{c}^{n}}'.format(s='', n=20, c='#')) - else: - print('{s:{c}^{n}}'.format(s='', n=40, c='#')) - print('# {:>10}'.format('Health: ') + - '{:6} #'.format(str(player.health)) + '# {:>10}'.format('Health: ') + - '{:6} #'.format(str(enemy.health))) - print('# {:>10}'.format('Strength: ') + - '{:6} #'.format(str(player.strength)) + '# {:>10}'.format('Strength: ') + - '{:6} #'.format(str(enemy.strength))) - print('# {:>10}'.format('Illbane: ') + - '{:6} #'.format(str(player.illbane)) + '# {:>10}'.format('Illbane: ') + - '{:6} #'.format(str(enemy.illbane))) - print('# {:>10}'.format('Gold: ') + - '{:6} #'.format(str(player.gold)) + '# {:>10}'.format('Gold: ') + - '{:6} #'.format(str(enemy.gold))) - print('{s:{c}^{n}}'.format(s='', n=40, c='#')) diff --git a/LinkedLists/doubly_linked_list.py b/LinkedLists/doubly_linked_list.py new file mode 100644 index 0000000..ed373a5 --- /dev/null +++ b/LinkedLists/doubly_linked_list.py @@ -0,0 +1,76 @@ +class Node(): + def __init__(self, payload): + self.payload = payload + self.prev = None + self.next = None + +class DoubleList(): + def __init__(self): + self.head = None + self.tail = None + + + def append(self, payload): + new_node = Node(payload) + new_node.prev = self.tail + + if self.tail is None: + self.head = self.tail = new_node + else: + temp_node = self.tail + self.tail.next = new_node + self.tail = self.tail.next + self.tail.prev = temp_node + + + def reverse(self): + current_node = self.head + temp_node = None + while current_node is not None: + temp_node = current_node.prev; + current_node.prev = current_node.next; + current_node.next = temp_node; + current_node = current_node.prev + + self.head = temp_node.prev + + + def remove(self, node_value): + current_node = self.head + + while current_node is not None: + if current_node.payload == node_value: + if current_node.prev is not None: + current_node.prev.next = current_node.next + current_node.next.prev = current_node.prev + else: + self.head = current_node.next + current_node.next.prev = None + + current_node = current_node.next + + + def show(self): + current_node = self.head + while current_node is not None: + print( current_node.payload) + current_node = current_node.next + + +d = DoubleList() + +d.append(1) +d.append(2) +d.append(3) +d.append(4) + +d.show() + +d.reverse() + +d.show() + +# d.remove(50) +# d.remove(5) + +# d.show() \ No newline at end of file diff --git a/LinkedLists/singly_linked_list.py b/LinkedLists/singly_linked_list.py new file mode 100644 index 0000000..b751173 --- /dev/null +++ b/LinkedLists/singly_linked_list.py @@ -0,0 +1,69 @@ +class Node(): + def __init__(self, payload): + self.payload = payload + self.next = None + +class DoubleList(): + def __init__(self): + self.head = None + self.tail = None + + def append(self, payload): + new_node = Node(payload) + new_node.next = None + + if (self.tail is not None): + self.tail.next = new_node + self.tail = self.tail.next + else: + self.head = new_node + self.tail = self.head + + def reverse(self): + current_node = self.head + last_node = None + while (current_node is not None): + temp_node = current_node.next + current_node.next, last_node = last_node, current_node + current_node = temp_node + + return last_node + + def reverse_r(self, current_node): + if (current_node is None): + return None + + temp_node = current_node.next; + if (temp_node is None): + return None + + self.reverse_r(temp_node); + temp_node.next = current_node; + current_node.next = None; + current_node = temp_node; + return self.tail + + def print_payload(self): + current_node = self.head + while current_node is not None: + print( current_node.payload) + current_node = current_node.next + + +d = DoubleList() + +d.append(1) +d.append(2) +d.append(3) +d.append(4) + +d.print_payload() + +d.head = d.reverse_r(d.head) + +d.print_payload() + +# d.remove(50) +# d.remove(5) + +# d.show() \ No newline at end of file diff --git a/PyLister/TODO.txt b/PyLister/TODO.txt deleted file mode 100755 index 0ca12f1..0000000 --- a/PyLister/TODO.txt +++ /dev/null @@ -1,11 +0,0 @@ -- Cleanup main.js -- Cleanup python code - -- Fix playing when not submitted (e.g. when clicking with the mouse before pressing enter) -- Add list shuffling -- Add prefixes: - - "shuffle:" adds shuffled results to the playlist - - "repeat:" adds results and repeats them. This replaces the current list - - "artist:", "album:", "title:" restrict search to related fields -- Add history of recently played songs -- Implement callback to play through shown items instead of having the path saved in the template diff --git a/PyLister/db.sqlite3 b/PyLister/db.sqlite3 deleted file mode 100644 index c6be983..0000000 Binary files a/PyLister/db.sqlite3 and /dev/null differ diff --git a/PyLister/lister/data_utils.py b/PyLister/lister/data_utils.py deleted file mode 100644 index c0eaa39..0000000 --- a/PyLister/lister/data_utils.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import connection -from django.http import JsonResponse - - -def data_for_songs_list(request, search_string=''): - if (search_string == ''): - return JsonResponse({}) - - all_search_words = search_string.split() - search_filters = [] - - if ':' in search_string: - search_filters = [search_word.split(':') - for search_word in all_search_words if ':' in search_word] - search_words = [ - search_word for search_word in all_search_words if ':' not in search_word] - - section = '' - search_params = [] - cursor = connection.cursor() - sql = 'SELECT title, lister_album.description album, lister_artist.description artist, image_file, path, year, track_number FROM lister_song, lister_album, lister_artist WHERE lister_artist.artist_id = lister_song.artist_id AND lister_album.album_id = lister_song.album_id ''' - - if (len(search_filters) > 0): - query_strings = [] - for search_filter in search_filters: - query_strings.append( - '((artist like %s or album like %s) and title like %s)') - filter_query = ' (' + ' OR '.join(query_strings) + ') ' - - for search_filter in search_filters: - search_params.append('%' + search_filter[0] + '%') - search_params.append('%' + search_filter[0] + '%') - search_params.append('%' + search_filter[1] + '%') - - if (len(search_words) > 0): - section = 'songs' - string_conditions = [] - for i in (range(len(search_words))): - string_conditions.append( - '( title like %s or lister_album.description like %s or lister_artist.description like %s)') - word_query = '(' + ' OR '.join(string_conditions) + ')' - for search_word in search_words: - for i in range(0, 3): - search_params.append('%' + search_word + '%') - - if (len(search_filters) > 0) and (len(search_words) > 0): - sql += ' AND (' + filter_query + ' OR ' + word_query + ')' - elif len(search_filters) > 0: - sql += ' AND ' + filter_query - elif len(search_words) > 0: - sql += ' AND ' + word_query - - sql += ' ORDER BY lister_song.artist_id, lister_album.album_id, track_number' - - songs_list = [] - track_index = 0 - cursor.execute(sql, search_params) - row = cursor.fetchone() - while (row): - row_type = track_index % 2 - songs_list.append( - {'title': row[0], 'album': row[1], 'artist': row[2], 'image_file': row[3], 'path': row[4], 'year': row[5], 'track': row[6], 'row_type': row_type, 'track_index': track_index}) - track_index += 1 - row = cursor.fetchone() - - context = {'songs_list': songs_list, - 'counters': get_counters(), 'section': section} - return JsonResponse(context) - - -def get_counters(): - counters = {} - cursor = connection.cursor() - cursor.execute('''SELECT count(*) FROM lister_song''') - counters['songs'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(*) FROM lister_album''') - counters['albums'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(*) FROM lister_artist''') - counters['artists'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(distinct year) FROM lister_song''') - counters['years'] = cursor.fetchone()[0] - - return counters diff --git a/PyLister/lister/templating_utils.py b/PyLister/lister/templating_utils.py deleted file mode 100755 index 9a5959c..0000000 --- a/PyLister/lister/templating_utils.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import connection -from django.template import loader -import data_utils as du - - -def render_wrapper(request): - template = loader.get_template('lister/wrapper.html') - context = {'counters': get_counters()} - - return template.render(context, request) - - -def render_for_songs_list(request, album='', artist='', year=''): - template = loader.get_template('lister/songs.html') - context = {} - return template.render(context, request) - - -def get_counters(): - counters = {} - cursor = connection.cursor() - cursor.execute('''SELECT count(*) FROM lister_song''') - counters['songs'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(*) FROM lister_album''') - counters['albums'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(*) FROM lister_artist''') - counters['artists'] = cursor.fetchone()[0] - cursor.execute('''SELECT count(distinct year) FROM lister_song''') - counters['years'] = cursor.fetchone()[0] - - return counters 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 + +![main interface](https://raw.githubusercontent.com/codezapper/PythonCodeExercises/master/SearchPlayer/search_player_screenshot.png) + +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/PyLister/PyLister/__init__.py b/SearchPlayer/SearchPlayer/__init__.py similarity index 100% rename from PyLister/PyLister/__init__.py rename to SearchPlayer/SearchPlayer/__init__.py diff --git a/PyLister/PyLister/settings.py b/SearchPlayer/SearchPlayer/settings.py similarity index 95% rename from PyLister/PyLister/settings.py rename to SearchPlayer/SearchPlayer/settings.py index 62b79e5..a6ba4c4 100755 --- a/PyLister/PyLister/settings.py +++ b/SearchPlayer/SearchPlayer/settings.py @@ -1,5 +1,5 @@ """ -Django settings for PyLister project. +Django settings for SearchPlayer project. Generated by 'django-admin startproject' using Django 1.10.1. @@ -51,7 +51,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'PyLister.urls' +ROOT_URLCONF = 'SearchPlayer.urls' TEMPLATES = [ { @@ -69,7 +69,7 @@ }, ] -WSGI_APPLICATION = 'PyLister.wsgi.application' +WSGI_APPLICATION = 'SearchPlayer.wsgi.application' # Database diff --git a/PyLister/PyLister/urls.py b/SearchPlayer/SearchPlayer/urls.py similarity index 91% rename from PyLister/PyLister/urls.py rename to SearchPlayer/SearchPlayer/urls.py index 1893361..c184b5b 100755 --- a/PyLister/PyLister/urls.py +++ b/SearchPlayer/SearchPlayer/urls.py @@ -1,4 +1,4 @@ -"""PyLister URL Configuration +"""SearchPlayer URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.10/topics/http/urls/ @@ -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/PyLister/PyLister/wsgi.py b/SearchPlayer/SearchPlayer/wsgi.py similarity index 72% rename from PyLister/PyLister/wsgi.py rename to SearchPlayer/SearchPlayer/wsgi.py index 701b922..4fb1d6a 100755 --- a/PyLister/PyLister/wsgi.py +++ b/SearchPlayer/SearchPlayer/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for PyLister project. +WSGI config for SearchPlayer project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyLister.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "SearchPlayer.settings") application = get_wsgi_application() diff --git a/SearchPlayer/TODO.txt b/SearchPlayer/TODO.txt new file mode 100755 index 0000000..83e32c3 --- /dev/null +++ b/SearchPlayer/TODO.txt @@ -0,0 +1,5 @@ +- Cleanup main.js + +- 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 new file mode 100644 index 0000000..83a8890 Binary files /dev/null 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/PyLister/file_indexer.py b/SearchPlayer/file_indexer.py similarity index 75% rename from PyLister/file_indexer.py rename to SearchPlayer/file_indexer.py index f80cbcb..6422cbb 100755 --- a/PyLister/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') @@ -43,6 +43,13 @@ def update_db(): cursor.executemany( 'INSERT INTO lister_artist (artist_id, description) VALUES(?, ?)', artists) + cursor.execute(''' + UPDATE lister_song SET search_key = (SELECT replace(lower(title||al.description||ar.description),' ','') + FROM lister_song so, lister_album al, lister_artist ar + WHERE ar.artist_id = so.artist_id + AND al.album_id = so.album_id + AND lister_song.id = so.id ) + ''') db_main.commit() diff --git a/PyLister/lister/__init__.py b/SearchPlayer/lister/__init__.py similarity index 100% rename from PyLister/lister/__init__.py rename to SearchPlayer/lister/__init__.py diff --git a/PyLister/lister/admin.py b/SearchPlayer/lister/admin.py similarity index 100% rename from PyLister/lister/admin.py rename to SearchPlayer/lister/admin.py diff --git a/PyLister/lister/apps.py b/SearchPlayer/lister/apps.py similarity index 100% rename from PyLister/lister/apps.py rename to SearchPlayer/lister/apps.py diff --git a/PyLister/lister/migrations/0001_initial.py b/SearchPlayer/lister/migrations/0001_initial.py similarity index 100% rename from PyLister/lister/migrations/0001_initial.py rename to SearchPlayer/lister/migrations/0001_initial.py diff --git a/PyLister/lister/migrations/0002_auto_20161105_2319.py b/SearchPlayer/lister/migrations/0002_auto_20161105_2319.py similarity index 100% rename from PyLister/lister/migrations/0002_auto_20161105_2319.py rename to SearchPlayer/lister/migrations/0002_auto_20161105_2319.py diff --git a/PyLister/lister/migrations/0003_auto_20161106_1812.py b/SearchPlayer/lister/migrations/0003_auto_20161106_1812.py similarity index 100% rename from PyLister/lister/migrations/0003_auto_20161106_1812.py rename to SearchPlayer/lister/migrations/0003_auto_20161106_1812.py diff --git a/PyLister/lister/migrations/0004_song_path.py b/SearchPlayer/lister/migrations/0004_song_path.py similarity index 100% rename from PyLister/lister/migrations/0004_song_path.py rename to SearchPlayer/lister/migrations/0004_song_path.py diff --git a/PyLister/lister/migrations/0005_song_track_number.py b/SearchPlayer/lister/migrations/0005_song_track_number.py similarity index 100% rename from PyLister/lister/migrations/0005_song_track_number.py rename to SearchPlayer/lister/migrations/0005_song_track_number.py diff --git a/PyLister/lister/migrations/0006_auto_20161124_2243.py b/SearchPlayer/lister/migrations/0006_auto_20161124_2243.py similarity index 100% rename from PyLister/lister/migrations/0006_auto_20161124_2243.py rename to SearchPlayer/lister/migrations/0006_auto_20161124_2243.py diff --git a/PyLister/lister/migrations/0007_auto_20161124_2244.py b/SearchPlayer/lister/migrations/0007_auto_20161124_2244.py similarity index 100% rename from PyLister/lister/migrations/0007_auto_20161124_2244.py rename to SearchPlayer/lister/migrations/0007_auto_20161124_2244.py diff --git a/PyLister/lister/migrations/0008_auto_20161124_2246.py b/SearchPlayer/lister/migrations/0008_auto_20161124_2246.py similarity index 100% rename from PyLister/lister/migrations/0008_auto_20161124_2246.py rename to SearchPlayer/lister/migrations/0008_auto_20161124_2246.py diff --git a/PyLister/lister/migrations/0009_auto_20161124_2247.py b/SearchPlayer/lister/migrations/0009_auto_20161124_2247.py similarity index 100% rename from PyLister/lister/migrations/0009_auto_20161124_2247.py rename to SearchPlayer/lister/migrations/0009_auto_20161124_2247.py diff --git a/PyLister/lister/migrations/0010_auto_20161124_2248.py b/SearchPlayer/lister/migrations/0010_auto_20161124_2248.py similarity index 100% rename from PyLister/lister/migrations/0010_auto_20161124_2248.py rename to SearchPlayer/lister/migrations/0010_auto_20161124_2248.py diff --git a/SearchPlayer/lister/migrations/0011_song_search_key.py b/SearchPlayer/lister/migrations/0011_song_search_key.py new file mode 100644 index 0000000..8b8bdf9 --- /dev/null +++ b/SearchPlayer/lister/migrations/0011_song_search_key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2017-01-28 08:16 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lister', '0010_auto_20161124_2248'), + ] + + operations = [ + migrations.AddField( + model_name='song', + name='search_key', + field=models.CharField(default='', max_length=1024), + ), + ] diff --git a/PyLister/lister/migrations/__init__.py b/SearchPlayer/lister/migrations/__init__.py similarity index 100% rename from PyLister/lister/migrations/__init__.py rename to SearchPlayer/lister/migrations/__init__.py diff --git a/PyLister/lister/models.py b/SearchPlayer/lister/models.py similarity index 94% rename from PyLister/lister/models.py rename to SearchPlayer/lister/models.py index a205d57..52b4a51 100755 --- a/PyLister/lister/models.py +++ b/SearchPlayer/lister/models.py @@ -12,6 +12,7 @@ class Song(models.Model): title = models.CharField(max_length=256) track_number = models.CharField(max_length=256, default='') path = models.CharField(max_length=256, default='') + search_key = models.CharField(max_length=1024, default='') def __str__(self): return '{} ({}) by {}'.format(self.title, self.year or '----', self.artist) 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/PyLister/lister/static/lister/images/missing_cover.png b/SearchPlayer/lister/static/lister/images/missing_cover.png similarity index 100% rename from PyLister/lister/static/lister/images/missing_cover.png rename to SearchPlayer/lister/static/lister/images/missing_cover.png diff --git a/PyLister/lister/static/lister/images/next_track.png b/SearchPlayer/lister/static/lister/images/next_track.png similarity index 100% rename from PyLister/lister/static/lister/images/next_track.png rename to SearchPlayer/lister/static/lister/images/next_track.png diff --git a/PyLister/lister/static/lister/images/next_track_hover.png b/SearchPlayer/lister/static/lister/images/next_track_hover.png similarity index 100% rename from PyLister/lister/static/lister/images/next_track_hover.png rename to SearchPlayer/lister/static/lister/images/next_track_hover.png diff --git a/PyLister/lister/static/lister/images/pause.png b/SearchPlayer/lister/static/lister/images/pause.png similarity index 100% rename from PyLister/lister/static/lister/images/pause.png rename to SearchPlayer/lister/static/lister/images/pause.png diff --git a/PyLister/lister/static/lister/images/pause_hover.png b/SearchPlayer/lister/static/lister/images/pause_hover.png similarity index 100% rename from PyLister/lister/static/lister/images/pause_hover.png rename to SearchPlayer/lister/static/lister/images/pause_hover.png diff --git a/PyLister/lister/static/lister/images/play.png b/SearchPlayer/lister/static/lister/images/play.png similarity index 100% rename from PyLister/lister/static/lister/images/play.png rename to SearchPlayer/lister/static/lister/images/play.png diff --git a/PyLister/lister/static/lister/images/play_hover.png b/SearchPlayer/lister/static/lister/images/play_hover.png similarity index 100% rename from PyLister/lister/static/lister/images/play_hover.png rename to SearchPlayer/lister/static/lister/images/play_hover.png diff --git a/PyLister/lister/static/lister/images/prev_track.png b/SearchPlayer/lister/static/lister/images/prev_track.png similarity index 100% rename from PyLister/lister/static/lister/images/prev_track.png rename to SearchPlayer/lister/static/lister/images/prev_track.png diff --git a/PyLister/lister/static/lister/images/prev_track_hover.png b/SearchPlayer/lister/static/lister/images/prev_track_hover.png similarity index 100% rename from PyLister/lister/static/lister/images/prev_track_hover.png rename to SearchPlayer/lister/static/lister/images/prev_track_hover.png diff --git a/PyLister/lister/static/lister/images/repeat_off.png b/SearchPlayer/lister/static/lister/images/repeat_off.png similarity index 100% rename from PyLister/lister/static/lister/images/repeat_off.png rename to SearchPlayer/lister/static/lister/images/repeat_off.png diff --git a/PyLister/lister/static/lister/images/repeat_on.png b/SearchPlayer/lister/static/lister/images/repeat_on.png similarity index 100% rename from PyLister/lister/static/lister/images/repeat_on.png rename to SearchPlayer/lister/static/lister/images/repeat_on.png diff --git a/PyLister/lister/static/lister/images/shuffle_off.png b/SearchPlayer/lister/static/lister/images/shuffle_off.png similarity index 100% rename from PyLister/lister/static/lister/images/shuffle_off.png rename to SearchPlayer/lister/static/lister/images/shuffle_off.png diff --git a/PyLister/lister/static/lister/images/shuffle_on.png b/SearchPlayer/lister/static/lister/images/shuffle_on.png similarity index 100% rename from PyLister/lister/static/lister/images/shuffle_on.png rename to SearchPlayer/lister/static/lister/images/shuffle_on.png diff --git a/PyLister/lister/static/lister/images/volume_down.png b/SearchPlayer/lister/static/lister/images/volume_down.png similarity index 100% rename from PyLister/lister/static/lister/images/volume_down.png rename to SearchPlayer/lister/static/lister/images/volume_down.png diff --git a/PyLister/lister/static/lister/images/volume_up.png b/SearchPlayer/lister/static/lister/images/volume_up.png similarity index 100% rename from PyLister/lister/static/lister/images/volume_up.png rename to SearchPlayer/lister/static/lister/images/volume_up.png diff --git a/PyLister/lister/static/lister/jquery-3.1.1.min.js b/SearchPlayer/lister/static/lister/jquery-3.1.1.min.js similarity index 100% rename from PyLister/lister/static/lister/jquery-3.1.1.min.js rename to SearchPlayer/lister/static/lister/jquery-3.1.1.min.js diff --git a/PyLister/lister/static/lister/jquery-ui.min.js b/SearchPlayer/lister/static/lister/jquery-ui.min.js similarity index 100% rename from PyLister/lister/static/lister/jquery-ui.min.js rename to SearchPlayer/lister/static/lister/jquery-ui.min.js diff --git a/PyLister/lister/static/lister/main.css b/SearchPlayer/lister/static/lister/main.css similarity index 79% rename from PyLister/lister/static/lister/main.css rename to SearchPlayer/lister/static/lister/main.css index 60cf8be..6d391e8 100755 --- a/PyLister/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/PyLister/lister/static/lister/main.js b/SearchPlayer/lister/static/lister/main.js similarity index 64% rename from PyLister/lister/static/lister/main.js rename to SearchPlayer/lister/static/lister/main.js index c9b50f4..6ab0186 100755 --- a/PyLister/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 = []; @@ -21,7 +23,8 @@ var duration = -1; var playListTemplate; var prevSearchTerm; var currentSearchTerm; -var hasSubmitted = false; +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' }); }); }); } @@ -74,7 +79,7 @@ function playTrack(track) { } else if (track > trackList.length - 1) { track = 0; } - $('[data-index-playlist=' + currentTrack + ']').closest('ul').removeClass('active-track'); + $('[data-index-playlist=' + trackList[currentTrack].song_id + ']').closest('ul').removeClass('active-track'); currentTrack = track; // Needed when clicking directly on the track duration = -1; @@ -82,32 +87,56 @@ 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'); playButton.addClass('pause-button'); - $('[data-index-playlist=' + currentTrack + ']').closest('ul').addClass('active-track'); + $('[data-index-playlist=' + trackList[currentTrack].song_id + ']').closest('ul').addClass('active-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; playTrack(0); } else if (operation === trackListOperations.PREPEND) { - trackList = searchResults.concat(trackList); + filteredSearchResults = searchResults.filter(function(song) { + if (displayedSongIDs[song.song_id] == null) { + displayedSongIDs[song.song_id] = 1; + return true; + } + return false; + }); + trackList = filteredSearchResults.concat(trackList); } else { - trackList = trackList.concat(searchResults); + filteredSearchResults = searchResults.filter(function(song) { + if (displayedSongIDs[song.song_id] == null) { + displayedSongIDs[song.song_id] = 1; + return true; + } + return false; + }); + 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() { @@ -116,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 }); @@ -125,26 +154,43 @@ 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") { - hasSubmitted = false; + 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=' + currentTrack + ']').closest('ul').addClass('active-track'); - } + mustRefresh = false; + updateDisplaySelectedTrack(); } } else { if (searchResults.length > 0) { if (prevSearchTerm !== currentSearchTerm) { - hasSubmitted = true; - inputBox.val(''); if (event.ctrlKey) { setCurrentTrackList(currentSearchTerm, searchResults, trackListOperations.REPLACE); } else if (event.shiftKey) { @@ -156,6 +202,7 @@ function bindUI() { if (playerElement.paused || !playerElement.currentTime) { playTrack(currentTrack); } + inputBox.val(''); } } }); @@ -219,4 +266,4 @@ function bindUI() { }); } -$(document).ready(bindUI); +$(document).ready(bindUI); \ No newline at end of file diff --git a/PyLister/lister/static/lister/mustache.js b/SearchPlayer/lister/static/lister/mustache.js similarity index 100% rename from PyLister/lister/static/lister/mustache.js rename to SearchPlayer/lister/static/lister/mustache.js diff --git a/PyLister/lister/templates/lister/detail.html b/SearchPlayer/lister/templates/lister/detail.html similarity index 100% rename from PyLister/lister/templates/lister/detail.html rename to SearchPlayer/lister/templates/lister/detail.html diff --git a/PyLister/lister/templates/lister/index.html b/SearchPlayer/lister/templates/lister/index.html similarity index 100% rename from PyLister/lister/templates/lister/index.html rename to SearchPlayer/lister/templates/lister/index.html diff --git a/PyLister/lister/templates/lister/main_content.html b/SearchPlayer/lister/templates/lister/main_content.html similarity index 100% rename from PyLister/lister/templates/lister/main_content.html rename to SearchPlayer/lister/templates/lister/main_content.html diff --git a/PyLister/lister/templates/lister/songs.html b/SearchPlayer/lister/templates/lister/songs.html similarity index 81% rename from PyLister/lister/templates/lister/songs.html rename to SearchPlayer/lister/templates/lister/songs.html index c25f12b..f185e0c 100644 --- a/PyLister/lister/templates/lister/songs.html +++ b/SearchPlayer/lister/templates/lister/songs.html @@ -13,7 +13,7 @@ {% verbatim %}{{#search_results}}