design: Break doc into pieces
Each component has its own text file. I merge everything together with simple dependency resolution so I can figure out implementation order easier. Signed-off-by: Bryan Schumaker <bjschuma@gmail.com>
This commit is contained in:
parent
b5d3a663e0
commit
e13b4afa60
2
config
2
config
|
@ -29,5 +29,5 @@ Export("release", "debug", "env", "CONFIG_RELEASE", "CONFIG_DEBUG")
|
|||
|
||||
# Import SConscript files
|
||||
include = SConscript("include/Sconscript")
|
||||
design = SConscript("design/Sconscript")
|
||||
tests = SConscript("tests/Sconscript")
|
||||
Alias("tests", tests)
|
||||
|
|
399
design.txt
399
design.txt
|
@ -10,20 +10,31 @@ out how everything works together, and this made Ocarina difficult to maintain
|
|||
because I had no overall plan. This document aims to fix that.
|
||||
|
||||
I will also create unit tests as I add features so bugs can be found faster.
|
||||
Unit tests will be created for each module (file) in my backend library code.
|
||||
|
||||
|
||||
|
||||
Files:
|
||||
$HOME/.ocarina{-debug}/
|
||||
album.db
|
||||
artist.db
|
||||
genre.db
|
||||
groups.idx
|
||||
library.db
|
||||
playlists.lst
|
||||
track.db
|
||||
/usr/bin/
|
||||
ocarina
|
||||
/usr/lib/ocarina/
|
||||
ocarina/
|
||||
design.txt
|
||||
ocarina/gui/
|
||||
ocarina/include/
|
||||
audio.h
|
||||
database.h
|
||||
database.hpp
|
||||
file.h
|
||||
filter.h
|
||||
group.h
|
||||
groups.h
|
||||
idle.h
|
||||
index.h
|
||||
library.h
|
||||
|
@ -35,22 +46,13 @@ Files:
|
|||
audio.cpp
|
||||
database.cpp
|
||||
file.cpp
|
||||
group.cpp
|
||||
filter.cpp
|
||||
groups.cpp
|
||||
idle.cpp
|
||||
index.cpp
|
||||
library.cpp
|
||||
playlist.cpp
|
||||
prefs.cpp
|
||||
ocarina/tests/
|
||||
|
||||
$HOME/.ocarina{-debug}/
|
||||
album.db
|
||||
artist.db
|
||||
genre.db
|
||||
groups.idx
|
||||
library.db
|
||||
playlists.lst
|
||||
track.db
|
||||
|
||||
|
||||
|
||||
|
@ -79,47 +81,6 @@ API:
|
|||
|
||||
|
||||
|
||||
Audio: (lib/audio.cpp)
|
||||
This file will introduce an "audio" namespace containing all of the
|
||||
functions interacting with gstreamer. This will create a wrapper
|
||||
namespace that will be easier to work with than using raw gstreamer
|
||||
functions.
|
||||
|
||||
The audio layer will also control the "pause after N tracks" feature
|
||||
so songs can be loaded without neeting to pass in a "begin playback"
|
||||
flag every time.
|
||||
|
||||
- Internal:
|
||||
Set up a message bus to look for end-of-stream and error messages so
|
||||
the next song can be played. This function should call the play
|
||||
function after loading a track and after checking the "pause after N"
|
||||
count.
|
||||
|
||||
- API:
|
||||
audio :: init(argc, argv)
|
||||
Initialize the gstreamer layer and reload the track that was
|
||||
last loaded before shutdown. Please only pass --gst-* options
|
||||
for argv.
|
||||
audio :: load_file(filepath)
|
||||
Loads a file path but does not begin playback.
|
||||
audio :: play()
|
||||
Begin playback
|
||||
audio :: pause()
|
||||
Pause playback
|
||||
audio :: seek_to(X)
|
||||
Seek to a position X seconds into the track
|
||||
audio :: stop()
|
||||
pause()
|
||||
seek_to(0)
|
||||
audio :: pause_after(N)
|
||||
Pause after N tracks, pass a negative number to disable.
|
||||
audio :: position()
|
||||
Return the number of seconds that the song has played
|
||||
audio :: duration()
|
||||
Return the duration of the current song in seconds
|
||||
|
||||
|
||||
|
||||
Idle queue: (lib/idle.cpp)
|
||||
The idle queue is used to schedule tasks to run at a later time. Idle
|
||||
tasks must inherit from the IdleBase class so that multiple templated
|
||||
|
@ -161,6 +122,47 @@ Idle queue: (lib/idle.cpp)
|
|||
|
||||
|
||||
|
||||
Audio: (lib/audio.cpp)
|
||||
This file will introduce an "audio" namespace containing all of the
|
||||
functions interacting with gstreamer. This will create a wrapper
|
||||
namespace that will be easier to work with than using raw gstreamer
|
||||
functions.
|
||||
|
||||
The audio layer will also control the "pause after N tracks" feature
|
||||
so songs can be loaded without neeting to pass in a "begin playback"
|
||||
flag every time.
|
||||
|
||||
- Internal:
|
||||
Set up a message bus to look for end-of-stream and error messages so
|
||||
the next song can be played. This function should call the play
|
||||
function after loading a track and after checking the "pause after N"
|
||||
count.
|
||||
|
||||
- API:
|
||||
audio :: init(argc, argv)
|
||||
Initialize the gstreamer layer and reload the track that was
|
||||
last loaded before shutdown. Please only pass --gst-* options
|
||||
for argv.
|
||||
audio :: load_file(filepath)
|
||||
Loads a file path but does not begin playback.
|
||||
audio :: play()
|
||||
Begin playback
|
||||
audio :: pause()
|
||||
Pause playback
|
||||
audio :: seek_to(X)
|
||||
Seek to a position X seconds into the track
|
||||
audio :: stop()
|
||||
pause()
|
||||
seek_to(0)
|
||||
audio :: pause_after(N)
|
||||
Pause after N tracks, pass a negative number to disable.
|
||||
audio :: position()
|
||||
Return the number of seconds that the song has played
|
||||
audio :: duration()
|
||||
Return the duration of the current song in seconds
|
||||
|
||||
|
||||
|
||||
On-disk files: (lib/file.cpp)
|
||||
I use the disk to store data between sessions, this could include
|
||||
library state and user preferences. In theory, file formats do not
|
||||
|
@ -224,6 +226,21 @@ On-disk files: (lib/file.cpp)
|
|||
|
||||
|
||||
|
||||
Preferences: (lib/prefs.cpp)
|
||||
Preferences make use of a special index where the set<int> is always
|
||||
size 1. Preferences will be in the prefs namespace.
|
||||
|
||||
- Index:
|
||||
Index prefs(prefs.idx);
|
||||
|
||||
- API:
|
||||
prefs :: set(string, val);
|
||||
prefs.replace(string, val);
|
||||
prefs :: get(string)
|
||||
return prefs[string].begin()
|
||||
|
||||
|
||||
|
||||
Database: (lib/database.cpp)
|
||||
Ocarina 5.x created a different save file format for each type of
|
||||
data that needed to be stored (preferences, library paths, playlists).
|
||||
|
@ -300,6 +317,70 @@ Database: (lib/database.cpp)
|
|||
|
||||
|
||||
|
||||
Playlist: (lib/playlist.cpp)
|
||||
Playlists are a list of songs that the user has configured to play. I
|
||||
will create a pool of playlists that will be filled by user actions.
|
||||
|
||||
Playlists will be put on a "deck" that is used to give an order to the
|
||||
next songs played. When deck :: next() is called, find the first
|
||||
playlist with PL_ENABLED set and call that playlists next() function.
|
||||
|
||||
When a playlist is empty, remove it from the deck.
|
||||
|
||||
- Flags:
|
||||
enum playlist_flags {
|
||||
PL_ENABLED (1 << 0),
|
||||
PL_RANDOM (1 << 1),
|
||||
PL_DRAIN (1 << 2),
|
||||
};
|
||||
|
||||
- Playlist:
|
||||
class Playlist : public Database {
|
||||
private:
|
||||
database<track_id> tracks; /* Keyed on track id */
|
||||
unsigned int cur;
|
||||
unsigned int flags;
|
||||
public:
|
||||
Playlist();
|
||||
void add(vector<track_id> &);
|
||||
void del(vector<track_id> &);
|
||||
void set_flag(playlist_flags);
|
||||
const unsigned int get_flags();
|
||||
unsigned int size()
|
||||
|
||||
File &operator<<(File &);
|
||||
File &operator>>(File &);
|
||||
|
||||
void sort();
|
||||
void next();
|
||||
}
|
||||
|
||||
File << flags << cur << tracks[0] << tracks[1] << ... << tracks[N];
|
||||
|
||||
- Deck:
|
||||
list<Playlist> deck;
|
||||
File << deck[0] << endl;
|
||||
File << deck[1] << endl;
|
||||
File << deck[N] << endl;
|
||||
|
||||
- API
|
||||
deck :: init();
|
||||
Read in the playlist file
|
||||
deck :: new();
|
||||
Adds a new playlist to the deck
|
||||
deck :: rm(N)
|
||||
Removes playlist N from the deck
|
||||
Playlist *deck :: get(N)
|
||||
Return playlist N from the deck
|
||||
|
||||
- TODO <<<<<
|
||||
What if each playlist has its own playlist_id for tracks? This would
|
||||
allow for simpler removals, since I won't need to search for a track id.
|
||||
I can easily create a function for mapping a list of playlist_ids to
|
||||
track_ids...
|
||||
|
||||
|
||||
|
||||
Index: (lib/index.cpp)
|
||||
An inverted index allows me to map multiple values to a single key.
|
||||
|
||||
|
@ -349,6 +430,68 @@ Index: (lib/index.cpp)
|
|||
|
||||
|
||||
|
||||
Filter: (lib/filter.cpp)
|
||||
Filtering is used to generate a subset of songs for easier searching.
|
||||
|
||||
- Index:
|
||||
map<string, string> lowercase_cache;
|
||||
map<string, set<string>> substring_cache;
|
||||
Index filter_index("");
|
||||
|
||||
- Parsing:
|
||||
1) Convert the provided string into a list of words, using whitespace
|
||||
and the following characters as delimiters: \/,;()_~+"
|
||||
|
||||
For each word:
|
||||
2) Check the lowercase_cache to see if we have seen the word before,
|
||||
a) If we have, return the stored string
|
||||
b) Convert the string to lowercase and strip out remaining
|
||||
special characters. Add the result to the lowercase_cache;
|
||||
3) Check the substring_cache to see if we have seen the word before,
|
||||
a) If we have, use the substring set returned
|
||||
b) Break the word into substrings from the front only. For
|
||||
example: "dalek" would contain the substrings
|
||||
{d, da, dal, dale, dalek}. Add to the substring cache.
|
||||
|
||||
- API:
|
||||
filter :: add(string, track_id);
|
||||
Parses the string and adds the track_id to the index.
|
||||
void filter :: search(string, set<track_id> &);
|
||||
Parse the string and fill in the set with matching tracks.
|
||||
|
||||
|
||||
|
||||
Groups: (lib/group.cpp)
|
||||
Groups are going to be a new feature in Ocarina 6 and can compare
|
||||
directly to Gmail-style labels. Ocarina 6 will create dynamic groups
|
||||
that cannot be deleted by the user based on library status. Similar
|
||||
to the library, groups should exist in their own namespace.
|
||||
|
||||
- Index:
|
||||
Index group_idx(groups.idx)
|
||||
|
||||
- Default groups:
|
||||
All music
|
||||
All tracks are added to this group
|
||||
Library
|
||||
Banned Songs
|
||||
These groups are mutually exclusive. A track is either
|
||||
in the Library or the Banned Songs group
|
||||
|
||||
- API
|
||||
group :: init();
|
||||
Initialize the index
|
||||
group :: list();
|
||||
return group_idx.keys();
|
||||
group :: get_tracks(name):
|
||||
return group_idx[name]
|
||||
group :: add(name, track_id)
|
||||
group_idx.insert(name, track_id);
|
||||
group :: del(name, track_id)
|
||||
grou_idx.delete(name, track_id)
|
||||
|
||||
|
||||
|
||||
Library: (lib/library.cpp)
|
||||
The library manages databases containing track information added by the
|
||||
user. Ocarina 6 splits the library into multiple database tables for
|
||||
|
@ -464,147 +607,6 @@ Library: (lib/library.cpp)
|
|||
|
||||
|
||||
|
||||
Groups: (lib/group.cpp)
|
||||
Groups are going to be a new feature in Ocarina 6 and can compare
|
||||
directly to Gmail-style labels. Ocarina 6 will create dynamic groups
|
||||
that cannot be deleted by the user based on library status. Similar
|
||||
to the library, groups should exist in their own namespace.
|
||||
|
||||
- Index:
|
||||
Index group_idx(groups.idx)
|
||||
|
||||
- Default groups:
|
||||
All music
|
||||
All tracks are added to this group
|
||||
Library
|
||||
Banned Songs
|
||||
These groups are mutually exclusive. A track is either
|
||||
in the Library or the Banned Songs group
|
||||
|
||||
- API
|
||||
group :: init();
|
||||
Initialize the index
|
||||
group :: list();
|
||||
return group_idx.keys();
|
||||
group :: get_tracks(name):
|
||||
return group_idx[name]
|
||||
group :: add(name, track_id)
|
||||
group_idx.insert(name, track_id);
|
||||
group :: del(name, track_id)
|
||||
grou_idx.delete(name, track_id)
|
||||
|
||||
|
||||
|
||||
Preferences: (lib/prefs.cpp)
|
||||
Preferences make use of a special index where the set<int> is always
|
||||
size 1. Preferences will be in the prefs namespace.
|
||||
|
||||
- Index:
|
||||
Index prefs(prefs.idx);
|
||||
|
||||
- API:
|
||||
prefs :: set(string, val);
|
||||
prefs.replace(string, val);
|
||||
prefs :: get(string)
|
||||
return prefs[string].begin()
|
||||
|
||||
|
||||
|
||||
Playlist: (lib/playlist.cpp)
|
||||
Playlists are a list of songs that the user has configured to play. I
|
||||
will create a pool of playlists that will be filled by user actions.
|
||||
|
||||
Playlists will be put on a "deck" that is used to give an order to the
|
||||
next songs played. When deck :: next() is called, find the first
|
||||
playlist with PL_ENABLED set and call that playlists next() function.
|
||||
|
||||
When a playlist is empty, remove it from the deck.
|
||||
|
||||
- Flags:
|
||||
enum playlist_flags {
|
||||
PL_ENABLED (1 << 0),
|
||||
PL_RANDOM (1 << 1),
|
||||
PL_DRAIN (1 << 2),
|
||||
};
|
||||
|
||||
- Playlist:
|
||||
class Playlist : public Database {
|
||||
private:
|
||||
database<track_id> tracks; /* Keyed on track id */
|
||||
unsigned int cur;
|
||||
unsigned int flags;
|
||||
public:
|
||||
Playlist();
|
||||
void add(vector<track_id> &);
|
||||
void del(vector<track_id> &);
|
||||
void set_flag(playlist_flags);
|
||||
const unsigned int get_flags();
|
||||
unsigned int size()
|
||||
|
||||
File &operator<<(File &);
|
||||
File &operator>>(File &);
|
||||
|
||||
void sort();
|
||||
void next();
|
||||
}
|
||||
|
||||
File << flags << cur << tracks[0] << tracks[1] << ... << tracks[N];
|
||||
|
||||
- Deck:
|
||||
list<Playlist> deck;
|
||||
File << deck[0] << endl;
|
||||
File << deck[1] << endl;
|
||||
File << deck[N] << endl;
|
||||
|
||||
- API
|
||||
deck :: init();
|
||||
Read in the playlist file
|
||||
deck :: new();
|
||||
Adds a new playlist to the deck
|
||||
deck :: rm(N)
|
||||
Removes playlist N from the deck
|
||||
Playlist *deck :: get(N)
|
||||
Return playlist N from the deck
|
||||
|
||||
- TODO <<<<<
|
||||
What if each playlist has its own playlist_id for tracks? This would
|
||||
allow for simpler removals, since I won't need to search for a track id.
|
||||
I can easily create a function for mapping a list of playlist_ids to
|
||||
track_ids...
|
||||
|
||||
|
||||
|
||||
Filter: (lib/filter.cpp)
|
||||
Filtering is used to generate a subset of songs for easier searching.
|
||||
|
||||
- Index:
|
||||
map<string, string> lowercase_cache;
|
||||
map<string, set<string>> substring_cache;
|
||||
Index filter_index("");
|
||||
|
||||
- Parsing:
|
||||
1) Convert the provided string into a list of words, using whitespace
|
||||
and the following characters as delimiters: \/,;()_~+"
|
||||
|
||||
For each word:
|
||||
2) Check the lowercase_cache to see if we have seen the word before,
|
||||
a) If we have, return the stored string
|
||||
b) Convert the string to lowercase and strip out remaining
|
||||
special characters. Add the result to the lowercase_cache;
|
||||
3) Check the substring_cache to see if we have seen the word before,
|
||||
a) If we have, use the substring set returned
|
||||
b) Break the word into substrings from the front only. For
|
||||
example: "dalek" would contain the substrings
|
||||
{d, da, dal, dale, dalek}. Add to the substring cache.
|
||||
|
||||
- API:
|
||||
filter :: add(string, track_id);
|
||||
Parses the string and adds the track_id to the index.
|
||||
void filter :: search(string, set<track_id> &);
|
||||
Parse the string and fill in the set with matching tracks.
|
||||
|
||||
|
||||
|
||||
Future work:
|
||||
I want to set reasonable expectations for Ocarina 6 so that I don't
|
||||
have to spend a large amount of time coding before releasing something
|
||||
|
@ -613,9 +615,6 @@ Future work:
|
|||
|
||||
Hint: If feature B depends on A, implement A in 6.x and B in 6.x+1
|
||||
|
||||
- Break design doc into pieces: (6.1)
|
||||
Createa script to join everything together cleanly.
|
||||
|
||||
- New default groups: (6.1)
|
||||
Unplayed tracks
|
||||
|
||||
|
@ -652,10 +651,6 @@ Future work:
|
|||
Make a pop-up window for editing the tags of a track. Be sure
|
||||
to update the library information and the on-disk file.
|
||||
|
||||
- Clean up Scons files / build system: (6.0) <<<<<
|
||||
I just had to try looking up something for Josh. I have no idea
|
||||
how my build system works anymore. This is bad.
|
||||
|
||||
- Album art: (6.1)
|
||||
|
||||
- Playlist custom sorting: (6.1)
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/python
|
||||
import os
|
||||
|
||||
created_files = dict()
|
||||
nodes = dict()
|
||||
|
||||
class DesignFile:
|
||||
def read_files(self, file):
|
||||
lastkey = ""
|
||||
for line in file:
|
||||
if line == "\n":
|
||||
return
|
||||
if line[0] == " " and line[1] == " ":
|
||||
created_files[lastkey] += [line.strip()]
|
||||
created_files[lastkey].sort()
|
||||
else:
|
||||
lastkey = line.strip()
|
||||
created_files.setdefault(lastkey, [])
|
||||
|
||||
def read_depends(self, file):
|
||||
for line in file:
|
||||
if line == "\n":
|
||||
return;
|
||||
else:
|
||||
self.depends += line.strip().split()
|
||||
|
||||
def __init__(self, file):
|
||||
self.name = ""
|
||||
self.lines = []
|
||||
self.depends = []
|
||||
self.depend_nodes = set()
|
||||
|
||||
if file == None:
|
||||
return
|
||||
self.name = file.rsplit(".", 1)[0]
|
||||
f = open("design/%s" % file)
|
||||
for line in f:
|
||||
if line == "== Files ==\n":
|
||||
self.read_files(f)
|
||||
elif line == "== Depends ==\n":
|
||||
self.read_depends(f)
|
||||
else:
|
||||
self.lines += [line]
|
||||
nodes[self.name] = self
|
||||
|
||||
|
||||
def pump_depends(nodes):
|
||||
res = None
|
||||
for n in nodes:
|
||||
if len(n.depend_nodes) == 0:
|
||||
res = n
|
||||
break
|
||||
nodes.remove(res)
|
||||
for n in nodes:
|
||||
if res in n.depend_nodes:
|
||||
n.depend_nodes.remove(res)
|
||||
return res
|
||||
|
||||
|
||||
def resolve_dependencies():
|
||||
tmp = set()
|
||||
res = []
|
||||
|
||||
for key, node in nodes.items():
|
||||
tmp.add(node)
|
||||
for depend in node.depends:
|
||||
if depend == "*":
|
||||
node.depend_nodes = set(nodes.values())
|
||||
else:
|
||||
node.depend_nodes.add(nodes[depend])
|
||||
node.depend_nodes.discard(nodes[node.name])
|
||||
|
||||
while len(tmp) > 0:
|
||||
res += [ pump_depends(tmp) ]
|
||||
return res
|
||||
|
||||
|
||||
def gen_files_list(parsed):
|
||||
design = DesignFile(None)
|
||||
design.lines += [ "Files:\n"]
|
||||
keys = created_files.keys()
|
||||
keys.sort()
|
||||
for key in keys:
|
||||
design.lines += [ " %s\n" % key ]
|
||||
for val in created_files[key]:
|
||||
design.lines += [ " %s\n" % val ]
|
||||
parsed.insert(1, design)
|
||||
|
||||
|
||||
def merge_design(target, source, env):
|
||||
files = os.listdir("design/")
|
||||
files.sort()
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
DesignFile(file)
|
||||
node_list = resolve_dependencies()
|
||||
gen_files_list(node_list)
|
||||
|
||||
f = open("design.txt", 'w')
|
||||
for index, design in enumerate(node_list):
|
||||
if index != 0:
|
||||
f.write("\n\n\n")
|
||||
for line in design.lines:
|
||||
f.write(line)
|
||||
|
||||
design = Command("design.txt", None, merge_design)
|
||||
Alias("design", design)
|
|
@ -0,0 +1,47 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
audio.h
|
||||
ocarina/lib/
|
||||
audio.cpp
|
||||
|
||||
== Depends ==
|
||||
version print
|
||||
|
||||
Audio: (lib/audio.cpp)
|
||||
This file will introduce an "audio" namespace containing all of the
|
||||
functions interacting with gstreamer. This will create a wrapper
|
||||
namespace that will be easier to work with than using raw gstreamer
|
||||
functions.
|
||||
|
||||
The audio layer will also control the "pause after N tracks" feature
|
||||
so songs can be loaded without neeting to pass in a "begin playback"
|
||||
flag every time.
|
||||
|
||||
- Internal:
|
||||
Set up a message bus to look for end-of-stream and error messages so
|
||||
the next song can be played. This function should call the play
|
||||
function after loading a track and after checking the "pause after N"
|
||||
count.
|
||||
|
||||
- API:
|
||||
audio :: init(argc, argv)
|
||||
Initialize the gstreamer layer and reload the track that was
|
||||
last loaded before shutdown. Please only pass --gst-* options
|
||||
for argv.
|
||||
audio :: load_file(filepath)
|
||||
Loads a file path but does not begin playback.
|
||||
audio :: play()
|
||||
Begin playback
|
||||
audio :: pause()
|
||||
Pause playback
|
||||
audio :: seek_to(X)
|
||||
Seek to a position X seconds into the track
|
||||
audio :: stop()
|
||||
pause()
|
||||
seek_to(0)
|
||||
audio :: pause_after(N)
|
||||
Pause after N tracks, pass a negative number to disable.
|
||||
audio :: position()
|
||||
Return the number of seconds that the song has played
|
||||
audio :: duration()
|
||||
Return the duration of the current song in seconds
|
|
@ -0,0 +1,83 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
database.h
|
||||
database.hpp
|
||||
ocarina/lib/
|
||||
database.cpp
|
||||
|
||||
== Depends ==
|
||||
idle file
|
||||
|
||||
Database: (lib/database.cpp)
|
||||
Ocarina 5.x created a different save file format for each type of
|
||||
data that needed to be stored (preferences, library paths, playlists).
|
||||
I intend to unify everything into a generic file format that can be
|
||||
accessed through a generic database interface. The database code will
|
||||
be in charge of printing the "valid" bit for each DatabaseEntry so that
|
||||
child classes do not need to call into the parent class. If valid ==
|
||||
true, the DatabaseEntry will be streamed out followed by a newline. If
|
||||
valid == false the database will print the next entry in the vector.
|
||||
|
||||
Modules should inherit from the DatabasEntry class and implement their
|
||||
own read() and write() functions. The "valid" field will be stored
|
||||
before these functions are called, and the entry will be skipped if
|
||||
valid is set to false.
|
||||
|
||||
The Database class is a templated class, so code could potentially
|
||||
get messy. Normal class declarations can still exist in the file
|
||||
include/database.h and member functions can be written in the file
|
||||
include/database.hpp, which will be included by database.h. Any
|
||||
function not relying on a template can be written in lib/database.cpp.
|
||||
|
||||
- DatabaseEntry:
|
||||
class DatabaseEntry { /* let database modify valid flag */
|
||||
private:
|
||||
bool valid;
|
||||
public:
|
||||
virtual File &operator>>(File &) = 0;
|
||||
virtual File &operator<<(File &) = 0;
|
||||
friend class Database;
|
||||
};
|
||||
|
||||
File << <CHILD_CLASS_DATA>
|
||||
|
||||
- Database:
|
||||
template <class T>
|
||||
class Database {
|
||||
private:
|
||||
unsigned int _size; /* Number of valid rows */
|
||||
File filename;
|
||||
vector<T> db;
|
||||
public:
|
||||
Database::Database(filename);
|
||||
void load();
|
||||
void save();
|
||||
unsigned int insert(T);
|
||||
void delete(unsigned int);
|
||||
const unsigned int &size();
|
||||
const T &operator[](unsigned int);
|
||||
};
|
||||
|
||||
File << db.size() << endl
|
||||
File << INDEX_0 << db[INDEX_0].valid << db[INDEX_0] << endl;
|
||||
File << Index_1 << db[INDEX_1].valid << db[INDEX_1] << endl;
|
||||
...
|
||||
|
||||
- API:
|
||||
Database.Database(filename);
|
||||
Initializes database to use ~/.ocarina{-debug}/filename. Pass
|
||||
an empty string if you do not want this database to be saved.
|
||||
Database.load();
|
||||
Reads data from file. Call after static initialization of
|
||||
Ocarina to ensure idle tasks are configured so loading can
|
||||
happen at a later time.
|
||||
Database.save();
|
||||
Saves data to file.
|
||||
Database.insert(T &);
|
||||
Adds a new item to the db, returns the id of the item
|
||||
Database.delete(unsigned int index);
|
||||
Mark db[index] as invalid (quick deletion)
|
||||
Database.size();
|
||||
Returns number of valid rows in the database
|
||||
Database.operator[unsigned int index]
|
||||
Return a reference to db[index]
|
|
@ -0,0 +1,69 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
file.h
|
||||
ocarina/lib/
|
||||
file.cpp
|
||||
|
||||
== Depends ==
|
||||
version print
|
||||
|
||||
On-disk files: (lib/file.cpp)
|
||||
I use the disk to store data between sessions, this could include
|
||||
library state and user preferences. In theory, file formats do not
|
||||
change often so updating between file formats should be possible.
|
||||
Supporting all previous file formats can create a lot of clutter in
|
||||
the code, so I will ONLY support updating from the previous file-format
|
||||
version. Ocarina 5.x used two different numbers to represent the
|
||||
current file format (library = 2 and playlist = 3). I want to unify
|
||||
this into a single number shared across all files for simplicity, and
|
||||
then create a class to read and write data on disk.
|
||||
|
||||
Items should be written to a file with either a space or new line
|
||||
separating multiple values.
|
||||
|
||||
- Notation:
|
||||
File << aaaaa << bbbbb << endl is translated into "aaaaa bbbbb\n"
|
||||
|
||||
- File version:
|
||||
#define FILE_VERSION 4
|
||||
|
||||
- Open mode:
|
||||
enum OpenMode {
|
||||
OPEN_READ,
|
||||
OPEN_WRITE,
|
||||
}
|
||||
|
||||
- File:
|
||||
class File {
|
||||
private:
|
||||
union {
|
||||
ifstream in;
|
||||
ofstream out;
|
||||
} f;
|
||||
unsigned int version;
|
||||
OpenMode mode;
|
||||
string filepath;
|
||||
public:
|
||||
File(filepath);
|
||||
open(OpenMode);
|
||||
const File &operator<<(File &);
|
||||
const File &operator>>(File &);
|
||||
getline(string &);
|
||||
}
|
||||
|
||||
- File format:
|
||||
File << FILE_VERSION << <OTHER_DATA>
|
||||
|
||||
- API:
|
||||
File : File(filepath)
|
||||
Resolve filepath to ~/.ocarina{-debug}/filepath
|
||||
File : open(mode)
|
||||
Open a file for either reading or writing.
|
||||
If reading: Read in version from start of file
|
||||
Else write version to file
|
||||
File : operator<<()
|
||||
Write data to out
|
||||
File : operator>>()
|
||||
Read data from in;
|
||||
File : getline(string);
|
||||
getline(f.in, string) to read an entire line
|
|
@ -0,0 +1,37 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
filter.h
|
||||
ocarina/lib/
|
||||
filter.cpp
|
||||
|
||||
== Depends ==
|
||||
index
|
||||
|
||||
Filter: (lib/filter.cpp)
|
||||
Filtering is used to generate a subset of songs for easier searching.
|
||||
|
||||
- Index:
|
||||
map<string, string> lowercase_cache;
|
||||
map<string, set<string>> substring_cache;
|
||||
Index filter_index("");
|
||||
|
||||
- Parsing:
|
||||
1) Convert the provided string into a list of words, using whitespace
|
||||
and the following characters as delimiters: \/,;()_~+"
|
||||
|
||||
For each word:
|
||||
2) Check the lowercase_cache to see if we have seen the word before,
|
||||
a) If we have, return the stored string
|
||||
b) Convert the string to lowercase and strip out remaining
|
||||
special characters. Add the result to the lowercase_cache;
|
||||
3) Check the substring_cache to see if we have seen the word before,
|
||||
a) If we have, use the substring set returned
|
||||
b) Break the word into substrings from the front only. For
|
||||
example: "dalek" would contain the substrings
|
||||
{d, da, dal, dale, dalek}. Add to the substring cache.
|
||||
|
||||
- API:
|
||||
filter :: add(string, track_id);
|
||||
Parses the string and adds the track_id to the index.
|
||||
void filter :: search(string, set<track_id> &);
|
||||
Parse the string and fill in the set with matching tracks.
|
|
@ -0,0 +1,54 @@
|
|||
== Depends ==
|
||||
*
|
||||
|
||||
Future work:
|
||||
I want to set reasonable expectations for Ocarina 6 so that I don't
|
||||
have to spend a large amount of time coding before releasing something
|
||||
to the wild. This section will be a list of features that I want, but
|
||||
should be deferred to a future release so basic support can be coded.
|
||||
|
||||
Hint: If feature B depends on A, implement A in 6.x and B in 6.x+1
|
||||
|
||||
- New default groups: (6.1)
|
||||
Unplayed tracks
|
||||
|
||||
- Categories: (6.1)
|
||||
Use these to make "groups of groups" for better organization.
|
||||
Different categories can include Album, Artist and Genere
|
||||
dynamic groups in addition to user created groups (see below)
|
||||
|
||||
The Artist, Album and Genre "tables" can be used to populate
|
||||
these categories.
|
||||
|
||||
- User created song groups: (6.2)
|
||||
Basic add and remove features can be implemented using the
|
||||
Library and Banned Songs groups. This will give me a chance
|
||||
to test saving groups on a small scale before letting users
|
||||
create anything they want.
|
||||
|
||||
- Save a user's playlist as a group: (6.2)
|
||||
|
||||
- Library defragment: (6.1)
|
||||
Ocarina 6.0 will leave holes in the library when tracks are
|
||||
deleted, potentially leading to fragmentation and larger-than-
|
||||
needed file sizes. A "defragment" utility can be created to
|
||||
clean up unused slots.
|
||||
|
||||
To help with fixing groups, a mapping of (old values) ->
|
||||
(new values) should be kept.
|
||||
|
||||
- Fix track durations: (6.1)
|
||||
Some tracks in my library are tagged with the wrong duration,
|
||||
so fix them as they are played.
|
||||
|
||||
- Track tag editor: (6.2)
|
||||
Make a pop-up window for editing the tags of a track. Be sure
|
||||
to update the library information and the on-disk file.
|
||||
|
||||
- Album art: (6.1)
|
||||
|
||||
- Playlist custom sorting: (6.1)
|
||||
Click column headers to choos sort order
|
||||
Keep a list of fields that the user has selected and place new
|
||||
fields in the front of this list. Use a recursive stable sort
|
||||
to do the sorting.
|
|
@ -0,0 +1,39 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
groups.h
|
||||
ocarina/lib/
|
||||
groups.cpp
|
||||
$HOME/.ocarina{-debug}/
|
||||
groups.idx
|
||||
|
||||
== Depends ==
|
||||
index
|
||||
|
||||
Groups: (lib/group.cpp)
|
||||
Groups are going to be a new feature in Ocarina 6 and can compare
|
||||
directly to Gmail-style labels. Ocarina 6 will create dynamic groups
|
||||
that cannot be deleted by the user based on library status. Similar
|
||||
to the library, groups should exist in their own namespace.
|
||||
|
||||
- Index:
|
||||
Index group_idx(groups.idx)
|
||||
|
||||
- Default groups:
|
||||
All music
|
||||
All tracks are added to this group
|
||||
Library
|
||||
Banned Songs
|
||||
These groups are mutually exclusive. A track is either
|
||||
in the Library or the Banned Songs group
|
||||
|
||||
- API
|
||||
group :: init();
|
||||
Initialize the index
|
||||
group :: list();
|
||||
return group_idx.keys();
|
||||
group :: get_tracks(name):
|
||||
return group_idx[name]
|
||||
group :: add(name, track_id)
|
||||
group_idx.insert(name, track_id);
|
||||
group :: del(name, track_id)
|
||||
grou_idx.delete(name, track_id)
|
|
@ -0,0 +1,17 @@
|
|||
== Files ==
|
||||
ocarina/
|
||||
design.txt
|
||||
|
||||
===============================================================================
|
||||
= =
|
||||
= Ocarina 6.0 =
|
||||
= =
|
||||
===============================================================================
|
||||
|
||||
My main goal for Ocarina 6.x is to plan out all of my actions before writing
|
||||
code. In the past I was adding features as I thought of them before thinking
|
||||
out how everything works together, and this made Ocarina difficult to maintain
|
||||
because I had no overall plan. This document aims to fix that.
|
||||
|
||||
I will also create unit tests as I add features so bugs can be found faster.
|
||||
Unit tests will be created for each module (file) in my backend library code.
|
|
@ -0,0 +1,47 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
idle.h
|
||||
ocarina/lib/
|
||||
idle.cpp
|
||||
|
||||
== Depends ==
|
||||
version print
|
||||
|
||||
Idle queue: (lib/idle.cpp)
|
||||
The idle queue is used to schedule tasks to run at a later time. Idle
|
||||
tasks must inherit from the IdleBase class so that multiple templated
|
||||
types can be placed on the same idle queue.
|
||||
|
||||
- IdleBase:
|
||||
class IdleBase {
|
||||
IdleBase();
|
||||
~IdleBase();
|
||||
virtual void run() = 0;
|
||||
};
|
||||
|
||||
- IdleTask:
|
||||
template <class T>
|
||||
class IdleTask : IdleBase {
|
||||
private:
|
||||
void (*func)(T *);
|
||||
T *data;
|
||||
|
||||
public:
|
||||
IdleTask(void (*)(T *), T *);
|
||||
void run();
|
||||
};
|
||||
|
||||
- Queue:
|
||||
deque(IdleTask *> idle_queue;
|
||||
float queued = 0.0
|
||||
float serviced = 0.0
|
||||
|
||||
- API:
|
||||
template <class T>
|
||||
void idle :: schedule(void (*)(T *), T *);
|
||||
Schedule a function to run later.
|
||||
queued++
|
||||
bool idle :: run_task()
|
||||
Run the next task on the queue. Return true if a task was
|
||||
found, and false otherwise.
|
||||
scheduled++, reset to zero if idle queue is empty.
|
|
@ -0,0 +1,55 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
index.h
|
||||
ocarina/lib/
|
||||
index.cpp
|
||||
|
||||
== Depends ==
|
||||
idle file
|
||||
|
||||
Index: (lib/index.cpp)
|
||||
An inverted index allows me to map multiple values to a single key.
|
||||
|
||||
- Index:
|
||||
class Index {
|
||||
private:
|
||||
map<string, set<int>>
|
||||
File filename;
|
||||
public:
|
||||
Index::Index(filename);
|
||||
void load();
|
||||
void save();
|
||||
void insert(key, int);
|
||||
void delete(key, int);
|
||||
void replace(key, int);
|
||||
const set<string> &keys();
|
||||
const set<int> &operator[](string);
|
||||
};
|
||||
|
||||
File << key << endl;
|
||||
File << map[key].size() << int_0 << int_1 << ... << int_n << endl;
|
||||
|
||||
- API:
|
||||
Index.Index(filename);
|
||||
Initializes an index using ~/.ocarina{-debug}K/filename. Pass
|
||||
an empty string if you do not want this index to be saved.
|
||||
Index.load();
|
||||
Reads data from a file. Call after static initialization of
|
||||
Ocarina to ensure idle tasks are configured
|
||||
Index.save();
|
||||
Saves data to file
|
||||
Index.insert(key, int);
|
||||
1) If key does not exist, create it.
|
||||
2) Add int to the list
|
||||
3) Index.save()
|
||||
Index.delete(key, int);
|
||||
1) Remove int from the set of values associated with key
|
||||
2) Do not delete key if set is empty
|
||||
3) Index.save()
|
||||
Index.replace(key, int)
|
||||
1) map[key].clear()
|
||||
2) insert(key, int)
|
||||
Index.keys();
|
||||
Return a set of index keys
|
||||
Index.operator[](string key);
|
||||
Return the set associated with key
|
|
@ -0,0 +1,12 @@
|
|||
== Files ==
|
||||
/usr/bin/
|
||||
ocarina
|
||||
/usr/lib/ocarina/
|
||||
|
||||
== Depends ==
|
||||
header
|
||||
|
||||
Install:
|
||||
Ocarina will be compiled into a single executable placed under
|
||||
/usr/bin/. Any extra files needed to run will be placed under
|
||||
/usr/lib/ocarina/.
|
|
@ -0,0 +1,127 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
library.h
|
||||
ocarina/lib/
|
||||
library.cpp
|
||||
$HOME/.ocarina{-debug}/
|
||||
album.db
|
||||
artist.db
|
||||
genre.db
|
||||
library.db
|
||||
track.db
|
||||
|
||||
== Depends ==
|
||||
idle database groups filter
|
||||
|
||||
Library: (lib/library.cpp)
|
||||
The library manages databases containing track information added by the
|
||||
user. Ocarina 6 splits the library into multiple database tables for
|
||||
storing content. The library will exist in a library namespace to
|
||||
to make functions and classes more unique.
|
||||
|
||||
When a library : Track is created, it should be added to the "Library"
|
||||
group if it is NOT a member of the banned songs group.
|
||||
|
||||
- Album:
|
||||
class library :: Album : public DatabaseEntry {
|
||||
string name;
|
||||
short year;
|
||||
};
|
||||
|
||||
File << year << name
|
||||
|
||||
- Artist:
|
||||
class library :: Artist : public DatabaseEntry {
|
||||
string name;
|
||||
};
|
||||
|
||||
File << name
|
||||
|
||||
- Genre:
|
||||
class library :: Genre : public DatabaseEntry {
|
||||
string name;
|
||||
};
|
||||
|
||||
File << name
|
||||
|
||||
- Path:
|
||||
class library :: Path : public DatabaseEntry {
|
||||
string root_path;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
File << enabled << root_path
|
||||
|
||||
- Track:
|
||||
class library :: Track : public DatabaseEntry {
|
||||
unsigned int artist_id;
|
||||
unsigned int album_id;
|
||||
unsigned int genre_id;
|
||||
unsigned int library_id;
|
||||
|
||||
short track;
|
||||
short last_year;
|
||||
short last_month;
|
||||
short last_day;
|
||||
unsigned int play_count;
|
||||
unsigned int length;
|
||||
string title;
|
||||
string length_str;
|
||||
string filepath;
|
||||
};
|
||||
|
||||
File << artist_id << album_id << genre_id << library_id << track << last_year
|
||||
File << last_year << last_month << last_day << play_count << length << endl
|
||||
File << title << endl;
|
||||
File << filepath << endl;
|
||||
|
||||
- Track: /* This struct lies outside the library namespace */
|
||||
struct Track {
|
||||
library :: Album *album;
|
||||
library :: Artist *artist;
|
||||
library :: Genre *genre;
|
||||
library :: Library *library;
|
||||
library :: Track *track;
|
||||
};
|
||||
|
||||
- Databases:
|
||||
Database<library :: Album> album_db(album.db);
|
||||
Database<library :: Artist> artist_db(artist.db);
|
||||
Database<library :: Album> genre_db(genre.db);
|
||||
Database<library :: Library> library_db(library.db);
|
||||
Database<library :: Track> track_db(track.db);
|
||||
|
||||
- Updating algorithm:
|
||||
set<pair<lib_id, track_path>> known_tracks;
|
||||
|
||||
1) For each track currently in the library, check if the track exists
|
||||
in the filesystem.
|
||||
1a) If the track does exist, add to the known_tracks map.
|
||||
1b) Else, mark track invalid.
|
||||
2) For each file in the scan directory, check if (lib_id, track_path)
|
||||
exists in the known_tracks map.
|
||||
2a) If the file is in the map, do nothing.
|
||||
2b) Else, add track to the library, to the groups "All Music" and
|
||||
"Library", and then to the filter index.
|
||||
3) Save all databases
|
||||
|
||||
The taglib library should be used for finding artist, album, etc. tags
|
||||
for each track.
|
||||
|
||||
Use idle tasks for step 2 to break up tagging new files into chunks.
|
||||
This way the user will still be able to use Ocarina and scanning can
|
||||
happen while idle.
|
||||
|
||||
- API
|
||||
library :: init();
|
||||
Initialize all databases
|
||||
library :: add_path(string dir);
|
||||
Add new row to paths table, update
|
||||
library :: del_path(unsigned int lib_id);
|
||||
Invalidate a path row
|
||||
library :: update_path(lib_id);
|
||||
Update the given library path, if valid.
|
||||
const Database<LibraryEntry> &library :: get_db();
|
||||
Returns the database containing library information.
|
||||
struct Track library :: resolve(track_id)
|
||||
Fill out a Track structure for the provided track_id
|
|
@ -0,0 +1,72 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
playlist.h
|
||||
ocarina/lib/
|
||||
playlist.cpp
|
||||
$HOME/.ocarina{-debug}/
|
||||
playlists.lst
|
||||
|
||||
== Depends ==
|
||||
database
|
||||
|
||||
Playlist: (lib/playlist.cpp)
|
||||
Playlists are a list of songs that the user has configured to play. I
|
||||
will create a pool of playlists that will be filled by user actions.
|
||||
|
||||
Playlists will be put on a "deck" that is used to give an order to the
|
||||
next songs played. When deck :: next() is called, find the first
|
||||
playlist with PL_ENABLED set and call that playlists next() function.
|
||||
|
||||
When a playlist is empty, remove it from the deck.
|
||||
|
||||
- Flags:
|
||||
enum playlist_flags {
|
||||
PL_ENABLED (1 << 0),
|
||||
PL_RANDOM (1 << 1),
|
||||
PL_DRAIN (1 << 2),
|
||||
};
|
||||
|
||||
- Playlist:
|
||||
class Playlist : public Database {
|
||||
private:
|
||||
database<track_id> tracks; /* Keyed on track id */
|
||||
unsigned int cur;
|
||||
unsigned int flags;
|
||||
public:
|
||||
Playlist();
|
||||
void add(vector<track_id> &);
|
||||
void del(vector<track_id> &);
|
||||
void set_flag(playlist_flags);
|
||||
const unsigned int get_flags();
|
||||
unsigned int size()
|
||||
|
||||
File &operator<<(File &);
|
||||
File &operator>>(File &);
|
||||
|
||||
void sort();
|
||||
void next();
|
||||
}
|
||||
|
||||
File << flags << cur << tracks[0] << tracks[1] << ... << tracks[N];
|
||||
|
||||
- Deck:
|
||||
list<Playlist> deck;
|
||||
File << deck[0] << endl;
|
||||
File << deck[1] << endl;
|
||||
File << deck[N] << endl;
|
||||
|
||||
- API
|
||||
deck :: init();
|
||||
Read in the playlist file
|
||||
deck :: new();
|
||||
Adds a new playlist to the deck
|
||||
deck :: rm(N)
|
||||
Removes playlist N from the deck
|
||||
Playlist *deck :: get(N)
|
||||
Return playlist N from the deck
|
||||
|
||||
- TODO <<<<<
|
||||
What if each playlist has its own playlist_id for tracks? This would
|
||||
allow for simpler removals, since I won't need to search for a track id.
|
||||
I can easily create a function for mapping a list of playlist_ids to
|
||||
track_ids...
|
|
@ -0,0 +1,21 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
prefs.h
|
||||
ocarina/lib/
|
||||
prefs.cpp
|
||||
|
||||
== Depends ==
|
||||
idle file
|
||||
|
||||
Preferences: (lib/prefs.cpp)
|
||||
Preferences make use of a special index where the set<int> is always
|
||||
size 1. Preferences will be in the prefs namespace.
|
||||
|
||||
- Index:
|
||||
Index prefs(prefs.idx);
|
||||
|
||||
- API:
|
||||
prefs :: set(string, val);
|
||||
prefs.replace(string, val);
|
||||
prefs :: get(string)
|
||||
return prefs[string].begin()
|
|
@ -0,0 +1,16 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
print.h
|
||||
|
||||
== Depends ==
|
||||
install
|
||||
|
||||
Printing: (include/print.h>
|
||||
Sometimes text needs to be printed to the screen so users (or debuggers)
|
||||
know what is going on.
|
||||
|
||||
API:
|
||||
void print(string fmt, ...)
|
||||
Print text to the screen
|
||||
void dprint(string fmt, ...)
|
||||
Print text to the screen when debugging is enabled
|
|
@ -0,0 +1,10 @@
|
|||
== Files ==
|
||||
ocarina/include/
|
||||
version.h
|
||||
|
||||
== Depends ==
|
||||
install
|
||||
|
||||
Versioning: (include/version.h)
|
||||
This file contains a simple function for returning a string stating
|
||||
the current version.
|
Loading…
Reference in New Issue