Merge branch 'wip/cd-ripping'

This commit is contained in:
Elias Projahn 2021-01-16 16:04:47 +01:00
commit 2b9cff885b
37 changed files with 2214 additions and 1136 deletions

View file

@ -7,6 +7,7 @@ edition = "2018"
anyhow = "1.0.33" anyhow = "1.0.33"
diesel = { version = "1.4.5", features = ["sqlite"] } diesel = { version = "1.4.5", features = ["sqlite"] }
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
discid = "0.4.4"
fragile = "1.0.0" fragile = "1.0.0"
futures = "0.3.6" futures = "0.3.6"
futures-channel = "0.3.5" futures-channel = "0.3.5"

View file

@ -1,19 +1,15 @@
DROP TABLE persons; PRAGMA defer_foreign_keys;
DROP TABLE instruments; DROP TABLE "persons";
DROP TABLE "instruments";
DROP TABLE "works";
DROP TABLE "instrumentations";
DROP TABLE "work_parts";
DROP TABLE "work_sections";
DROP TABLE "ensembles";
DROP TABLE "recordings";
DROP TABLE "performances";
DROP TABLE "mediums";
DROP TABLE "track_sets";
DROP TABLE "tracks";
DROP TABLE works;
DROP TABLE instrumentations;
DROP TABLE work_parts;
DROP TABLE work_sections;
DROP TABLE ensembles;
DROP TABLE recordings;
DROP TABLE performances;
DROP TABLE tracks;

View file

@ -1,64 +1,78 @@
CREATE TABLE persons ( CREATE TABLE "persons" (
id TEXT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
first_name TEXT NOT NULL, "first_name" TEXT NOT NULL,
last_name TEXT NOT NULL "last_name" TEXT NOT NULL
); );
CREATE TABLE instruments ( CREATE TABLE "instruments" (
id TEXT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL "name" TEXT NOT NULL
); );
CREATE TABLE works ( CREATE TABLE "works" (
id TEXT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
composer TEXT NOT NULL REFERENCES persons(id), "composer" TEXT NOT NULL REFERENCES "persons"("id"),
title TEXT NOT NULL "title" TEXT NOT NULL
); );
CREATE TABLE instrumentations ( CREATE TABLE "instrumentations" (
id BIGINT NOT NULL PRIMARY KEY, "id" BIGINT NOT NULL PRIMARY KEY,
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE
); );
CREATE TABLE work_parts ( CREATE TABLE "work_parts" (
id BIGINT NOT NULL PRIMARY KEY, "id" BIGINT NOT NULL PRIMARY KEY,
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
part_index BIGINT NOT NULL, "part_index" BIGINT NOT NULL,
title TEXT NOT NULL, "title" TEXT NOT NULL,
composer TEXT REFERENCES persons(id) "composer" TEXT REFERENCES "persons"("id")
); );
CREATE TABLE work_sections ( CREATE TABLE "work_sections" (
id BIGINT NOT NULL PRIMARY KEY, "id" BIGINT NOT NULL PRIMARY KEY,
work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE,
title TEXT NOT NULL, "title" TEXT NOT NULL,
before_index BIGINT NOT NULL "before_index" BIGINT NOT NULL
); );
CREATE TABLE ensembles ( CREATE TABLE "ensembles" (
id TEXT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL "name" TEXT NOT NULL
); );
CREATE TABLE recordings ( CREATE TABLE "recordings" (
id TEXT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
work TEXT NOT NULL REFERENCES works(id), "work" TEXT NOT NULL REFERENCES "works"("id"),
comment TEXT NOT NULL "comment" TEXT NOT NULL
); );
CREATE TABLE performances ( CREATE TABLE "performances" (
id BIGINT NOT NULL PRIMARY KEY, "id" BIGINT NOT NULL PRIMARY KEY,
recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE,
person TEXT REFERENCES persons(id), "person" TEXT REFERENCES "persons"("id"),
ensemble TEXT REFERENCES ensembles(id), "ensemble" TEXT REFERENCES "ensembles"("id"),
role TEXT REFERENCES instruments(id) "role" TEXT REFERENCES "instruments"("id")
); );
CREATE TABLE tracks ( CREATE TABLE "mediums" (
id BIGINT NOT NULL PRIMARY KEY, "id" TEXT NOT NULL PRIMARY KEY,
file_name TEXT NOT NULL, "name" TEXT NOT NULL,
recording TEXT NOT NULL REFERENCES recordings(id), "discid" TEXT
track_index INTEGER NOT NULL,
work_parts TEXT NOT NULL
); );
CREATE TABLE "track_sets" (
"id" TEXT NOT NULL PRIMARY KEY,
"medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE,
"index" INTEGER NOT NULL,
"recording" TEXT NOT NULL REFERENCES "recordings"("id")
);
CREATE TABLE "tracks" (
"id" TEXT NOT NULL PRIMARY KEY,
"track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE,
"index" INTEGER NOT NULL,
"work_parts" TEXT NOT NULL,
"path" TEXT NOT NULL
);

View file

@ -2,11 +2,12 @@
<gresources> <gresources>
<gresource prefix="/de/johrpan/musicus"> <gresource prefix="/de/johrpan/musicus">
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file> <file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_selector.ui</file> <file preprocess="xml-stripblanks">ui/instrument_selector.ui</file>
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file> <file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file> <file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_editor.ui</file> <file preprocess="xml-stripblanks">ui/person_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_list.ui</file> <file preprocess="xml-stripblanks">ui/person_list.ui</file>
@ -22,8 +23,10 @@
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file> <file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
<file preprocess="xml-stripblanks">ui/selector.ui</file> <file preprocess="xml-stripblanks">ui/selector.ui</file>
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file> <file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/tracks_editor.ui</file> <file preprocess="xml-stripblanks">ui/source_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_editor.ui</file> <file preprocess="xml-stripblanks">ui/track_editor.ui</file>
<file preprocess="xml-stripblanks">ui/track_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_set_editor.ui</file>
<file preprocess="xml-stripblanks">ui/window.ui</file> <file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/work_editor.ui</file> <file preprocess="xml-stripblanks">ui/work_editor.ui</file>
<file preprocess="xml-stripblanks">ui/work_part_editor.ui</file> <file preprocess="xml-stripblanks">ui/work_part_editor.ui</file>

View file

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkStack" id="widget">
<property name="visible">True</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Import music</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="done_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<child>
<object class="GtkStack" id="done_stack">
<property name="visible">True</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="active">True</property>
</object>
</child>
<child>
<object class="GtkImage" id="done">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="revealed">False</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label" translatable="yes">Medium</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="HdyActionRow" id="name_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Name of the medium</property>
<property name="activatable-widget">name_entry</property>
<child>
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="HdyActionRow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Publish to the server</property>
<property name="activatable-widget">publish_switch</property>
<child>
<object class="GtkSwitch" id="publish_switch">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="active">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
</packing>
</child>
</object>
</interface>

View file

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Import music</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="message-type">error</property>
<property name="revealed">False</property>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Failed to load the CD. Make sure you have inserted it into your drive.</property>
<property name="wrap">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="pixel-size">80</property>
<property name="icon-name">media-optical-cd-audio-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Import from audio CD</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5019607843137255</property>
<property name="label" translatable="yes">Insert an audio compact disc into your drive and click the button below. The disc will be copied in the background while you set up the metadata.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="import_button">
<property name="label" translatable="yes">Import</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">start</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">loading</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,107 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.24"/> <requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/> <requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget"> <object class="GtkBox" id="widget">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="HdyHeaderBar"> <object class="HdyHeaderBar">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Track</property> <property name="title" translatable="yes">Track</property>
<child> <child>
<object class="GtkButton" id="back_button"> <object class="GtkButton" id="back_button">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">True</property> <property name="can-focus">True</property>
<property name="receives-default">True</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property> <property name="icon-name">go-previous-symbolic</property>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkButton" id="save_button"> <object class="GtkButton" id="select_button">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">True</property> <property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style> <style>
<class name="suggested-action"/> <class name="suggested-action"/>
</style> </style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="pack-type">end</property> <property name="pack-type">end</property>
<property name="position">1</property>
</packing> </packing>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">True</property> <property name="can-focus">True</property>
<property name="vexpand">True</property>
<child> <child>
<object class="GtkViewport"> <object class="GtkViewport">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property> <property name="shadow-type">none</property>
<child> <child>
<object class="HdyClamp"> <object class="HdyClamp">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child> <child>
<object class="GtkFrame"> <object class="GtkFrame" id="parts_frame">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">start</property> <property name="valign">start</property>
<property name="margin-top">12</property>
<property name="margin-start">6</property> <property name="margin-start">6</property>
<property name="margin-end">6</property> <property name="margin-end">6</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property> <property name="margin-bottom">6</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property> <property name="shadow-type">in</property>
<child>
<object class="GtkListBox" id="list">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="label" translatable="yes">Select a recording of a work with multiple parts.</property>
<property name="ellipsize">end</property>
</object>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -109,11 +69,6 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child> </child>
</object> </object>
</interface> </interface>

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Select tracks</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="select_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkFrame" id="tracks_frame">
<property name="visible">True</property>
<property name="valign">start</property>
<property name="margin-top">12</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Tracks</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="sensitive">False</property>
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label" translatable="yes">Recording</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="HdyActionRow" id="recording_row">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">Select a recording</property>
<property name="activatable-widget">select_recording_button</property>
<child>
<object class="GtkButton" id="select_recording_button">
<property name="label" translatable="yes">Select</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="valign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Tracks</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkButton" id="edit_tracks_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">document-edit-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame" id="tracks_frame">
<property name="visible">True</property>
<property name="shadow-type">in</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,311 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Tracks</property>
<child>
<object class="GtkButton" id="save_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="maximum-size">800</property>
<property name="tightening-threshold">300</property>
<child>
<!-- n-columns=2 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="border-width">18</property>
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Recording</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="recording_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="hexpand">True</property>
<child>
<object class="GtkStack" id="recording_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vhomogeneous">False</property>
<property name="transition-type">crossfade</property>
<property name="interpolate-size">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Select …</property>
</object>
<packing>
<property name="name">unselected</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="work_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Work</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="performers_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="opacity">0.5</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Performers</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">selected</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="add_track_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="edit_track_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_track_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="move_track_down_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-down-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="move_track_up_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-up-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View file

@ -327,6 +327,10 @@
</object> </object>
<menu id="menu"> <menu id="menu">
<section> <section>
<item>
<attribute name="label" translatable="yes">Import CD</attribute>
<attribute name="action">win.import-disc</attribute>
</item>
<item> <item>
<attribute name="label" translatable="yes">Preferences</attribute> <attribute name="label" translatable="yes">Preferences</attribute>
<attribute name="action">win.preferences</attribute> <attribute name="action">win.preferences</attribute>

View file

@ -0,0 +1,254 @@
use super::generate_id;
use super::schema::{mediums, recordings, track_sets, tracks};
use super::{Database, Recording};
use anyhow::{anyhow, Error, Result};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
/// Representation of someting like a physical audio disc or a folder with
/// audio files (i.e. a collection of tracks for one or more recordings).
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Medium {
/// An unique ID for the medium.
pub id: String,
/// The human identifier for the medium.
pub name: String,
/// If applicable, the MusicBrainz DiscID.
pub discid: Option<String>,
/// The tracks of the medium, grouped by recording.
pub tracks: Vec<TrackSet>,
}
/// A set of tracks of one recording within a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TrackSet {
/// The recording to which the tracks belong.
pub recording: Recording,
/// The actual tracks.
pub tracks: Vec<Track>,
}
/// A track within a recording on a medium.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Track {
/// The work parts that are played on this track. They are indices to the
/// work parts of the work that is associated with the recording.
pub work_parts: Vec<usize>,
/// The path to the audio file containing this track. This will not be
/// included when communicating with the server.
#[serde(skip)]
pub path: String,
}
/// Table data for a [`Medium`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "mediums"]
struct MediumRow {
pub id: String,
pub name: String,
pub discid: Option<String>,
}
/// Table data for a [`TrackSet`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "track_sets"]
struct TrackSetRow {
pub id: String,
pub medium: String,
pub index: i32,
pub recording: String,
}
/// Table data for a [`Track`].
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "tracks"]
struct TrackRow {
pub id: String,
pub track_set: String,
pub index: i32,
pub work_parts: String,
pub path: String,
}
impl Database {
/// Update an existing medium or insert a new one.
pub fn update_medium(&self, medium: Medium) -> Result<()> {
self.defer_foreign_keys()?;
self.connection.transaction::<(), Error, _>(|| {
let medium_id = &medium.id;
// This will also delete the track sets and tracks.
self.delete_medium(medium_id)?;
// Add the new medium.
let medium_row = MediumRow {
id: medium_id.to_owned(),
name: medium.name.clone(),
discid: medium.discid.clone(),
};
diesel::insert_into(mediums::table)
.values(medium_row)
.execute(&self.connection)?;
for (index, track_set) in medium.tracks.iter().enumerate() {
// Add associated items from the server, if they don't already
// exist.
if self.get_recording(&track_set.recording.id)?.is_none() {
self.update_recording(track_set.recording.clone())?;
}
// Add the actual track set data.
let track_set_id = generate_id();
let track_set_row = TrackSetRow {
id: track_set_id.clone(),
medium: medium_id.to_owned(),
index: index as i32,
recording: track_set.recording.id.clone(),
};
diesel::insert_into(track_sets::table)
.values(track_set_row)
.execute(&self.connection)?;
for (index, track) in track_set.tracks.iter().enumerate() {
let work_parts = track
.work_parts
.iter()
.map(|part_index| part_index.to_string())
.collect::<Vec<String>>()
.join(",");
let track_row = TrackRow {
id: generate_id(),
track_set: track_set_id.clone(),
index: index as i32,
work_parts,
path: track.path.clone(),
};
diesel::insert_into(tracks::table)
.values(track_row)
.execute(&self.connection)?;
}
}
Ok(())
})?;
Ok(())
}
/// Get an existing medium.
pub fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
let row = mediums::table
.filter(mediums::id.eq(id))
.load::<MediumRow>(&self.connection)?
.into_iter()
.next();
let medium = match row {
Some(row) => Some(self.get_medium_data(row)?),
None => None,
};
Ok(medium)
}
/// Delete a medium and all of its tracks. This will fail, if the music
/// library contains audio files referencing any of those tracks.
pub fn delete_medium(&self, id: &str) -> Result<()> {
diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(&self.connection)?;
Ok(())
}
/// Get all available track sets for a recording.
pub fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
let mut track_sets: Vec<TrackSet> = Vec::new();
let rows = track_sets::table
.inner_join(recordings::table.on(recordings::id.eq(track_sets::recording)))
.filter(recordings::id.eq(recording_id))
.select(track_sets::table::all_columns())
.load::<TrackSetRow>(&self.connection)?;
for row in rows {
let track_set = self.get_track_set_from_row(row)?;
track_sets.push(track_set);
}
Ok(track_sets)
}
/// Retrieve all available information on a medium from related tables.
fn get_medium_data(&self, row: MediumRow) -> Result<Medium> {
let track_set_rows = track_sets::table
.filter(track_sets::medium.eq(&row.id))
.order_by(track_sets::index)
.load::<TrackSetRow>(&self.connection)?;
let mut track_sets = Vec::new();
for track_set_row in track_set_rows {
let track_set = self.get_track_set_from_row(track_set_row)?;
track_sets.push(track_set);
}
let medium = Medium {
id: row.id,
name: row.name,
discid: row.discid,
tracks: track_sets,
};
Ok(medium)
}
/// Convert a track set row from the database to an actual track set.
fn get_track_set_from_row(&self, row: TrackSetRow) -> Result<TrackSet> {
let recording_id = row.recording;
let recording = self
.get_recording(&recording_id)?
.ok_or_else(|| anyhow!("No recording with ID: {}", recording_id))?;
let track_rows = tracks::table
.filter(tracks::track_set.eq(row.id))
.order_by(tracks::index)
.load::<TrackRow>(&self.connection)?;
let mut tracks = Vec::new();
for track_row in track_rows {
let work_parts = track_row
.work_parts
.split(',')
.map(|part_index| Ok(str::parse(part_index)?))
.collect::<Result<Vec<usize>>>()?;
let track = Track {
work_parts,
path: track_row.path,
};
tracks.push(track);
}
let track_set = TrackSet { recording, tracks };
Ok(track_set)
}
}

View file

@ -7,6 +7,9 @@ pub use ensembles::*;
pub mod instruments; pub mod instruments;
pub use instruments::*; pub use instruments::*;
pub mod medium;
pub use medium::*;
pub mod persons; pub mod persons;
pub use persons::*; pub use persons::*;
@ -16,9 +19,6 @@ pub use recordings::*;
pub mod thread; pub mod thread;
pub use thread::*; pub use thread::*;
pub mod tracks;
pub use tracks::*;
pub mod works; pub mod works;
pub use works::*; pub use works::*;

View file

@ -190,6 +190,22 @@ impl Database {
Ok(exists) Ok(exists)
} }
/// Get an existing recording.
pub fn get_recording(&self, id: &str) -> Result<Option<Recording>> {
let row = recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(&self.connection)?
.into_iter()
.next();
let recording = match row {
Some(row) => Some(self.get_recording_data(row)?),
None => None,
};
Ok(recording)
}
/// Retrieve all available information on a recording from related tables. /// Retrieve all available information on a recording from related tables.
fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> { fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> {
let mut performance_descriptions: Vec<Performance> = Vec::new(); let mut performance_descriptions: Vec<Performance> = Vec::new();

View file

@ -20,6 +20,14 @@ table! {
} }
} }
table! {
mediums (id) {
id -> Text,
name -> Text,
discid -> Nullable<Text>,
}
}
table! { table! {
performances (id) { performances (id) {
id -> BigInt, id -> BigInt,
@ -47,12 +55,21 @@ table! {
} }
table! { table! {
tracks (id) { track_sets (id) {
id -> BigInt, id -> Text,
file_name -> Text, medium -> Text,
index -> Integer,
recording -> Text, recording -> Text,
track_index -> Integer, }
}
table! {
tracks (id) {
id -> Text,
track_set -> Text,
index -> Integer,
work_parts -> Text, work_parts -> Text,
path -> Text,
} }
} }
@ -90,7 +107,9 @@ joinable!(performances -> instruments (role));
joinable!(performances -> persons (person)); joinable!(performances -> persons (person));
joinable!(performances -> recordings (recording)); joinable!(performances -> recordings (recording));
joinable!(recordings -> works (work)); joinable!(recordings -> works (work));
joinable!(tracks -> recordings (recording)); joinable!(track_sets -> mediums (medium));
joinable!(track_sets -> recordings (recording));
joinable!(tracks -> track_sets (track_set));
joinable!(work_parts -> persons (composer)); joinable!(work_parts -> persons (composer));
joinable!(work_parts -> works (work)); joinable!(work_parts -> works (work));
joinable!(work_sections -> works (work)); joinable!(work_sections -> works (work));
@ -100,9 +119,11 @@ allow_tables_to_appear_in_same_query!(
ensembles, ensembles,
instrumentations, instrumentations,
instruments, instruments,
mediums,
performances, performances,
persons, persons,
recordings, recordings,
track_sets,
tracks, tracks,
work_parts, work_parts,
work_sections, work_sections,

View file

@ -28,9 +28,10 @@ enum Action {
GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>), GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>),
GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>), GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>),
RecordingExists(String, Sender<Result<bool>>), RecordingExists(String, Sender<Result<bool>>),
UpdateTracks(String, Vec<Track>, Sender<Result<()>>), UpdateMedium(Medium, Sender<Result<()>>),
DeleteTracks(String, Sender<Result<()>>), GetMedium(String, Sender<Result<Option<Medium>>>),
GetTracks(String, Sender<Result<Vec<Track>>>), DeleteMedium(String, Sender<Result<()>>),
GetTrackSets(String, Sender<Result<Vec<TrackSet>>>),
Stop(Sender<()>), Stop(Sender<()>),
} }
@ -124,16 +125,17 @@ impl DbThread {
RecordingExists(id, sender) => { RecordingExists(id, sender) => {
sender.send(db.recording_exists(&id)).unwrap(); sender.send(db.recording_exists(&id)).unwrap();
} }
UpdateTracks(recording_id, tracks, sender) => { UpdateMedium(medium, sender) => {
sender sender.send(db.update_medium(medium)).unwrap();
.send(db.update_tracks(&recording_id, tracks))
.unwrap();
} }
DeleteTracks(recording_id, sender) => { GetMedium(id, sender) => {
sender.send(db.delete_tracks(&recording_id)).unwrap(); sender.send(db.get_medium(&id)).unwrap();
} }
GetTracks(recording_id, sender) => { DeleteMedium(id, sender) => {
sender.send(db.get_tracks(&recording_id)).unwrap(); sender.send(db.delete_medium(&id)).unwrap();
}
GetTrackSets(recording_id, sender) => {
sender.send(db.get_track_sets(&recording_id)).unwrap();
} }
Stop(sender) => { Stop(sender) => {
sender.send(()).unwrap(); sender.send(()).unwrap();
@ -312,28 +314,35 @@ impl DbThread {
receiver.await? receiver.await?
} }
/// Add or change the tracks associated with a recording. This will fail, if there are still /// Update an existing medium or insert a new one.
/// other items referencing this recording. pub async fn update_medium(&self, medium: Medium) -> Result<()> {
pub async fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
self.action_sender self.action_sender.send(UpdateMedium(medium, sender))?;
.send(UpdateTracks(recording_id.to_string(), tracks, sender))?;
receiver.await? receiver.await?
} }
/// Delete all tracks associated with a recording. /// Delete an existing medium. This will fail, if there are still other
pub async fn delete_tracks(&self, recording_id: &str) -> Result<()> { /// items referencing this medium.
pub async fn delete_medium(&self, id: &str) -> Result<()> {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
self.action_sender self.action_sender
.send(DeleteTracks(recording_id.to_string(), sender))?; .send(DeleteMedium(id.to_owned(), sender))?;
receiver.await? receiver.await?
} }
/// Get all tracks associated with a recording. /// Get an existing medium.
pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> { pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> {
let (sender, receiver) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
self.action_sender self.action_sender.send(GetMedium(id.to_owned(), sender))?;
.send(GetTracks(recording_id.to_string(), sender))?; receiver.await?
}
/// Get all track sets for a recording.
pub async fn get_track_sets(&self, recording_id: &str) -> Result<Vec<TrackSet>> {
let (sender, receiver) = oneshot::channel();
self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?;
receiver.await? receiver.await?
} }

View file

@ -1,94 +0,0 @@
use super::schema::tracks;
use super::Database;
use anyhow::{Error, Result};
use diesel::prelude::*;
use std::convert::{TryFrom, TryInto};
/// Table row data for a track.
#[derive(Insertable, Queryable, Debug, Clone)]
#[table_name = "tracks"]
struct TrackRow {
pub id: i64,
pub file_name: String,
pub recording: String,
pub track_index: i32,
pub work_parts: String,
}
/// A structure representing one playable audio file.
#[derive(Debug, Clone)]
pub struct Track {
pub work_parts: Vec<usize>,
pub file_name: String,
}
impl TryFrom<TrackRow> for Track {
type Error = Error;
fn try_from(row: TrackRow) -> Result<Self> {
let mut work_parts = Vec::<usize>::new();
for part in row.work_parts.split(",") {
if !part.is_empty() {
work_parts.push(part.parse()?);
}
}
let track = Track {
work_parts,
file_name: row.file_name,
};
Ok(track)
}
}
impl Database {
/// Insert or update tracks for the specified recording.
pub fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> {
self.delete_tracks(recording_id)?;
for (index, track) in tracks.iter().enumerate() {
let row = TrackRow {
id: rand::random(),
file_name: track.file_name.clone(),
recording: recording_id.to_string(),
track_index: index.try_into()?,
work_parts: track
.work_parts
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join(","),
};
diesel::insert_into(tracks::table)
.values(row)
.execute(&self.connection)?;
}
Ok(())
}
/// Delete all tracks for the specified recording.
pub fn delete_tracks(&self, recording_id: &str) -> Result<()> {
diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id)))
.execute(&self.connection)?;
Ok(())
}
/// Get all tracks of the specified recording.
pub fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> {
let mut tracks = Vec::<Track>::new();
let rows = tracks::table
.filter(tracks::recording.eq(recording_id))
.order_by(tracks::track_index)
.load::<TrackRow>(&self.connection)?;
for row in rows {
tracks.push(row.try_into()?);
}
Ok(tracks)
}
}

View file

@ -10,13 +10,9 @@ pub use person::*;
pub mod recording; pub mod recording;
pub use recording::*; pub use recording::*;
pub mod tracks;
pub use tracks::*;
pub mod work; pub mod work;
pub use work::*; pub use work::*;
mod performance; mod performance;
mod track;
mod work_part; mod work_part;
mod work_section; mod work_section;

View file

@ -1,148 +0,0 @@
use crate::database::*;
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::convert::TryInto;
use std::rc::Rc;
/// A screen for editing a single track.
// TODO: Refactor.
pub struct TrackEditor {
widget: gtk::Box,
ready_cb: RefCell<Option<Box<dyn Fn(Track) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackEditor {
/// Create a new track editor.
pub fn new(track: Track, work: Work) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::ListBox, list);
let this = Rc::new(Self {
widget,
ready_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
let work = Rc::new(work);
let work_parts = Rc::new(RefCell::new(track.work_parts));
let file_name = track.file_name;
save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| {
let mut work_parts = work_parts.borrow_mut();
work_parts.sort();
if let Some(cb) = &*this.ready_cb.borrow() {
cb(Track {
work_parts: work_parts.clone(),
file_name: file_name.clone(),
});
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
for (index, part) in work.parts.iter().enumerate() {
let check = gtk::CheckButton::new();
check.set_active(work_parts.borrow().contains(&index));
check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| {
if check.get_active() {
let mut work_parts = work_parts.borrow_mut();
work_parts.push(index);
} else {
let mut work_parts = work_parts.borrow_mut();
if let Some(pos) = work_parts.iter().position(|part| *part == index) {
work_parts.remove(pos);
}
}
}));
let label = gtk::Label::new(Some(&part.title));
label.set_halign(gtk::Align::Start);
label.set_ellipsize(pango::EllipsizeMode::End);
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
hbox.set_border_width(6);
hbox.add(&check);
hbox.add(&label);
let row = gtk::ListBoxRow::new();
row.add(&hbox);
row.show_all();
list.add(&row);
list.connect_row_activated(
clone!(@strong row, @strong check => move |_, activated_row| {
if *activated_row == row {
check.activate();
}
}),
);
}
let mut section_count = 0;
for section in &work.sections {
let attributes = pango::AttrList::new();
attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap());
let label = gtk::Label::new(Some(&section.title));
label.set_halign(gtk::Align::Start);
label.set_ellipsize(pango::EllipsizeMode::End);
label.set_attributes(Some(&attributes));
let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0);
wrap.set_border_width(6);
wrap.add(&label);
let row = gtk::ListBoxRow::new();
row.set_activatable(false);
row.add(&wrap);
row.show_all();
list.insert(
&row,
(section.before_index + section_count).try_into().unwrap(),
);
section_count += 1;
}
this
}
/// Set the closure to be called when the track was edited.
pub fn set_ready_cb<F: Fn(Track) -> () + 'static>(&self, cb: F) {
self.ready_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for TrackEditor {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,337 +0,0 @@
use super::track::TrackEditor;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::{List, Navigator, NavigatorScreen};
use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector};
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a set of tracks.
// TODO: Disable buttons if no track is selected.
pub struct TracksEditor {
backend: Rc<Backend>,
widget: gtk::Box,
save_button: gtk::Button,
recording_stack: gtk::Stack,
work_label: gtk::Label,
performers_label: gtk::Label,
track_list: Rc<List<Track>>,
recording: RefCell<Option<Recording>>,
tracks: RefCell<Vec<Track>>,
callback: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TracksEditor {
/// Create a new track editor an optionally initialize it with a recording and a list of
/// tracks.
pub fn new(
backend: Rc<Backend>,
recording: Option<Recording>,
tracks: Vec<Track>,
) -> Rc<Self> {
// UI setup
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, recording_button);
get_widget!(builder, gtk::Stack, recording_stack);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Label, performers_label);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_track_button);
get_widget!(builder, gtk::Button, edit_track_button);
get_widget!(builder, gtk::Button, remove_track_button);
get_widget!(builder, gtk::Button, move_track_up_button);
get_widget!(builder, gtk::Button, move_track_down_button);
let track_list = List::new(&gettext("Add some tracks."));
scroll.add(&track_list.widget);
let this = Rc::new(Self {
backend,
widget,
save_button,
recording_stack,
work_label,
performers_label,
track_list,
recording: RefCell::new(recording),
tracks: RefCell::new(tracks),
callback: RefCell::new(None),
navigator: RefCell::new(None),
});
// Signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let this = this.clone();
context.spawn_local(async move {
let recording = this.recording.borrow().as_ref().unwrap().clone();
// Add the recording first, if it's from the server.
if !this.backend.db().recording_exists(&recording.id).await.unwrap() {
this.backend.db().update_recording(recording.clone()).await.unwrap();
}
// Add the actual tracks.
this.backend.db().update_tracks(
&recording.id,
this.tracks.borrow().clone(),
).await.unwrap();
if let Some(callback) = &*this.callback.borrow() {
callback();
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
});
}));
recording_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let person_selector = PersonSelector::new(this.backend.clone());
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
this.recording_selected(recording);
this.recording.replace(Some(recording.clone()));
navigator.clone().pop();
navigator.clone().pop();
navigator.clone().pop();
}));
navigator.clone().push(recording_selector);
}));
navigator.clone().push(work_selector);
}));
navigator.clone().push(person_selector);
}
}));
this.track_list
.set_make_widget(clone!(@strong this => move |track| {
this.build_track_row(track)
}));
add_track_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let music_library_path = this.backend.get_music_library_path().unwrap();
let dialog = gtk::FileChooserNative::new(
Some(&gettext("Select audio files")),
Some(&navigator.window),
gtk::FileChooserAction::Open,
None,
None,
);
dialog.set_select_multiple(true);
dialog.set_current_folder(&music_library_path);
if let gtk::ResponseType::Accept = dialog.run() {
let mut index = match this.track_list.get_selected_index() {
Some(index) => index + 1,
None => this.tracks.borrow().len(),
};
{
let mut tracks = this.tracks.borrow_mut();
for file_name in dialog.get_filenames() {
let file_name = file_name.strip_prefix(&music_library_path).unwrap();
tracks.insert(index, Track {
work_parts: Vec::new(),
file_name: String::from(file_name.to_str().unwrap()),
});
index += 1;
}
}
this.track_list.show_items(this.tracks.borrow().clone());
this.autofill_parts();
this.track_list.select_index(index);
}
}
}));
remove_track_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
let mut tracks = this.tracks.borrow_mut();
tracks.remove(index);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index);
}
None => (),
}
}));
move_track_up_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
if index > 0 {
let mut tracks = this.tracks.borrow_mut();
tracks.swap(index - 1, index);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index - 1);
}
}
None => (),
}
}));
move_track_down_button.connect_clicked(clone!(@strong this => move |_| {
match this.track_list.get_selected_index() {
Some(index) => {
let mut tracks = this.tracks.borrow_mut();
if index < tracks.len() - 1 {
tracks.swap(index, index + 1);
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index + 1);
}
}
None => (),
}
}));
edit_track_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
if let Some(index) = this.track_list.get_selected_index() {
if let Some(recording) = &*this.recording.borrow() {
let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone());
editor.set_ready_cb(clone!(@strong this => move |track| {
let mut tracks = this.tracks.borrow_mut();
tracks[index] = track;
this.track_list.show_items(tracks.clone());
this.track_list.select_index(index);
}));
navigator.push(editor);
}
}
}
}));
// Initialization
if let Some(recording) = &*this.recording.borrow() {
this.recording_selected(recording);
}
this.track_list.show_items(this.tracks.borrow().clone());
this
}
/// Set a callback to be called when the tracks are saved.
pub fn set_callback<F: Fn() -> () + 'static>(&self, cb: F) {
self.callback.replace(Some(Box::new(cb)));
}
/// Create a widget representing a track.
fn build_track_row(&self, track: &Track) -> gtk::Widget {
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
if let Some(recording) = &*self.recording.borrow() {
title_parts.push(recording.work.parts[*part].title.clone());
}
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let title_label = gtk::Label::new(Some(&title));
title_label.set_ellipsize(pango::EllipsizeMode::End);
title_label.set_halign(gtk::Align::Start);
let file_name_label = gtk::Label::new(Some(&track.file_name));
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
file_name_label.set_opacity(0.5);
file_name_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6);
vbox.add(&title_label);
vbox.add(&file_name_label);
vbox.upcast()
}
/// Set everything up after selecting a recording.
fn recording_selected(&self, recording: &Recording) {
self.work_label.set_text(&recording.work.get_title());
self.performers_label.set_text(&recording.get_performers());
self.recording_stack.set_visible_child_name("selected");
self.save_button.set_sensitive(true);
self.autofill_parts();
}
/// Automatically try to put work part information from the selected recording into the
/// selected tracks.
fn autofill_parts(&self) {
if let Some(recording) = &*self.recording.borrow() {
let mut tracks = self.tracks.borrow_mut();
for (index, _) in recording.work.parts.iter().enumerate() {
if let Some(mut track) = tracks.get_mut(index) {
track.work_parts = vec![index];
} else {
break;
}
}
self.track_list.show_items(tracks.clone());
}
}
}
impl NavigatorScreen for TracksEditor {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,175 @@
use anyhow::{anyhow, bail, Result};
use discid::DiscId;
use futures_channel::oneshot;
use gstreamer::prelude::*;
use gstreamer::{Element, ElementFactory, Pipeline};
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::thread;
/// Representation of an audio CD being imported as a medium.
#[derive(Clone, Debug)]
pub struct DiscSource {
/// The MusicBrainz DiscID of the CD.
pub discid: String,
/// The path to the temporary directory where the audio files will be.
pub path: PathBuf,
/// The tracks on this disc.
pub tracks: Vec<TrackSource>,
}
/// Representation of a single track on an audio CD.
#[derive(Clone, Debug)]
pub struct TrackSource {
/// The track number. This is different from the index in the disc
/// source's tracks list, because it is not defined from which number the
/// the track numbers start.
pub number: u32,
/// The path to the temporary file to which the track will be ripped. The
/// file will not exist until the track is actually ripped.
pub path: PathBuf,
}
impl DiscSource {
/// Try to create a new disc source by asynchronously reading the
/// information from the default disc drive.
pub async fn load() -> Result<Self> {
let (sender, receiver) = oneshot::channel();
thread::spawn(|| {
let disc = Self::load_priv();
sender.send(disc).unwrap();
});
let disc = receiver.await??;
Ok(disc)
}
/// Rip the whole disc asynchronously. After this method has finished
/// successfully, the audio files will be available in the specified
/// location for each track source.
pub async fn rip(&self) -> Result<()> {
for track in &self.tracks {
let (sender, receiver) = oneshot::channel();
let number = track.number;
let path = track.path.clone();
thread::spawn(move || {
let result = Self::rip_track(&path, number);
sender.send(result).unwrap();
});
receiver.await??;
}
Ok(())
}
/// Load the disc from the default disc drive.
fn load_priv() -> Result<Self> {
let discid = DiscId::read(None)?;
let id = discid.id();
let mut tracks = Vec::new();
let first_track = discid.first_track_num() as u32;
let last_track = discid.last_track_num() as u32;
let tmp_dir = Self::create_tmp_dir()?;
for number in first_track..=last_track {
let file_name = format!("track_{:02}.flac", number);
let mut path = tmp_dir.clone();
path.push(file_name);
let track = TrackSource {
number,
path,
};
tracks.push(track);
}
let disc = DiscSource {
discid: id,
tracks,
path: tmp_dir,
};
Ok(disc)
}
/// Create a new temporary directory and return its path.
// TODO: Move to a more appropriate place.
fn create_tmp_dir() -> Result<PathBuf> {
let mut tmp_dir = glib::get_tmp_dir()
.ok_or_else(|| {
anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!")
})?;
let dir_name = format!("musicus-{}", rand::random::<u64>());
tmp_dir.push(dir_name);
std::fs::create_dir(&tmp_dir)?;
Ok(tmp_dir)
}
/// Rip one track.
fn rip_track(path: &Path, number: u32) -> Result<()> {
let pipeline = Self::build_pipeline(path, number)?;
let bus = pipeline
.get_bus()
.ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?;
pipeline.set_state(gstreamer::State::Playing)?;
for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) {
use gstreamer::MessageView::*;
match msg.view() {
Eos(..) => break,
Error(err) => {
pipeline.set_state(gstreamer::State::Null)?;
bail!("GStreamer error: {:?}!", err);
}
_ => (),
}
}
pipeline.set_state(gstreamer::State::Null)?;
Ok(())
}
/// Build the GStreamer pipeline to rip a track.
fn build_pipeline(path: &Path, number: u32) -> Result<Pipeline> {
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
cdparanoiasrc.set_property("track", &number)?;
let queue = ElementFactory::make("queue", None)?;
let audioconvert = ElementFactory::make("audioconvert", None)?;
let flacenc = ElementFactory::make("flacenc", None)?;
let path_str = path.to_str().ok_or_else(|| {
anyhow!("Failed to convert path '{:?}' to string!", path)
})?;
let filesink = gstreamer::ElementFactory::make("filesink", None)?;
filesink.set_property("location", &path_str.to_owned())?;
let pipeline = gstreamer::Pipeline::new(None);
pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Ok(pipeline)
}
}

View file

@ -0,0 +1,241 @@
use super::disc_source::DiscSource;
use super::track_set_editor::{TrackSetData, TrackSetEditor};
use crate::database::{generate_id, Medium, Track, TrackSet};
use crate::backend::Backend;
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use anyhow::Result;
use glib::clone;
use glib::prelude::*;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing metadata while importing music into the music library.
pub struct MediumEditor {
backend: Rc<Backend>,
source: Rc<DiscSource>,
widget: gtk::Stack,
done_button: gtk::Button,
done_stack: gtk::Stack,
done: gtk::Image,
name_entry: gtk::Entry,
publish_switch: gtk::Switch,
track_set_list: List,
track_sets: RefCell<Vec<TrackSetData>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl MediumEditor {
/// Create a new medium editor.
pub fn new(backend: Rc<Backend>, source: DiscSource) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui");
get_widget!(builder, gtk::Stack, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, done_button);
get_widget!(builder, gtk::Stack, done_stack);
get_widget!(builder, gtk::Image, done);
get_widget!(builder, gtk::Entry, name_entry);
get_widget!(builder, gtk::Switch, publish_switch);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Frame, frame);
let list = List::new("No recordings added.");
frame.add(&list.widget);
let this = Rc::new(Self {
backend,
source: Rc::new(source),
widget,
done_button,
done_stack,
done,
name_entry,
publish_switch,
track_set_list: list,
track_sets: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.done_button.connect_clicked(clone!(@strong this => move |_| {
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
clone.widget.set_visible_child_name("loading");
match clone.clone().save().await {
Ok(_) => (),
Err(err) => {
println!("{:?}", err);
// clone.info_bar.set_revealed(true);
}
}
});
}));
add_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source));
editor.set_done_cb(clone!(@strong this => move |track_set| {
let length = {
let mut track_sets = this.track_sets.borrow_mut();
track_sets.push(track_set);
track_sets.len()
};
this.track_set_list.update(length);
}));
navigator.push(editor);
}
}));
this.track_set_list.set_make_widget(clone!(@strong this => move |index| {
let track_set = &this.track_sets.borrow()[index];
let title = track_set.recording.work.get_title();
let subtitle = track_set.recording.get_performers();
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
let edit_button = gtk::Button::new();
edit_button.set_relief(gtk::ReliefStyle::None);
edit_button.set_valign(gtk::Align::Center);
edit_button.add(&edit_image);
let row = libhandy::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(&subtitle));
row.add(&edit_button);
row.set_activatable_widget(Some(&edit_button));
row.show_all();
edit_button.connect_clicked(clone!(@strong this => move |_| {
}));
row.upcast()
}));
// Start ripping the CD in the background.
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match clone.source.rip().await {
Err(error) => {
// TODO: Present error.
println!("Failed to rip: {}", error);
},
Ok(_) => {
clone.done_stack.set_visible_child(&clone.done);
clone.done_button.set_sensitive(true);
}
}
});
this
}
/// Save the medium and possibly upload it to the server.
async fn save(self: Rc<Self>) -> Result<()> {
let name = self.name_entry.get_text().to_string();
// Create a new directory in the music library path for the imported medium.
let mut path = self.backend.get_music_library_path().unwrap().clone();
path.push(&name);
std::fs::create_dir(&path)?;
// Convert the track set data to real track sets.
let mut track_sets = Vec::new();
for track_set_data in &*self.track_sets.borrow() {
let mut tracks = Vec::new();
for track_data in &track_set_data.tracks {
// Copy the corresponding audio file to the music library.
let track_source = &self.source.tracks[track_data.track_source];
let file_name = format!("track_{:02}.flac", track_source.number);
let mut track_path = path.clone();
track_path.push(&file_name);
std::fs::copy(&track_source.path, &track_path)?;
// Create the real track.
let track = Track {
work_parts: track_data.work_parts.clone(),
path: track_path.to_str().unwrap().to_owned(),
};
tracks.push(track);
}
let track_set = TrackSet {
recording: track_set_data.recording.clone(),
tracks,
};
track_sets.push(track_set);
}
let medium = Medium {
id: generate_id(),
name: self.name_entry.get_text().to_string(),
discid: Some(self.source.discid.clone()),
tracks: track_sets,
};
let upload = self.publish_switch.get_active();
if upload {
// self.backend.post_medium(&medium).await?;
}
self.backend
.db()
.update_medium(medium.clone())
.await?;
self.backend.library_changed();
let navigator = self.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
Ok(())
}
}
impl NavigatorScreen for MediumEditor {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,8 @@
mod disc_source;
mod medium_editor;
mod source_selector;
mod track_editor;
mod track_selector;
mod track_set_editor;
pub use source_selector::SourceSelector;

View file

@ -0,0 +1,92 @@
use super::medium_editor::MediumEditor;
use super::disc_source::DiscSource;
use crate::backend::Backend;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for starting to import music.
pub struct SourceSelector {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl SourceSelector {
/// Create a new source selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, import_button);
let this = Rc::new(Self {
backend,
widget,
stack,
info_bar,
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
import_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match DiscSource::load().await {
Ok(disc) => {
let navigator = clone.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = MediumEditor::new(clone.backend.clone(), disc);
navigator.push(editor);
}
clone.info_bar.set_revealed(false);
clone.stack.set_visible_child_name("start");
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.stack.set_visible_child_name("start");
}
}
});
}));
this
}
}
impl NavigatorScreen for SourceSelector {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,110 @@
use crate::database::Recording;
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for editing a single track.
pub struct TrackEditor {
widget: gtk::Box,
selection: RefCell<Vec<usize>>,
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackEditor {
/// Create a new track editor.
pub fn new(recording: Recording, selection: Vec<usize>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, parts_frame);
let parts_list = gtk::ListBox::new();
parts_list.set_selection_mode(gtk::SelectionMode::None);
parts_list.set_vexpand(false);
parts_list.show();
parts_frame.add(&parts_list);
let this = Rc::new(Self {
widget,
selection: RefCell::new(selection),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
select_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
if let Some(cb) = &*this.selected_cb.borrow() {
let selection = this.selection.borrow().clone();
cb(selection);
}
}));
for (index, part) in recording.work.parts.iter().enumerate() {
let check = gtk::CheckButton::new();
check.set_active(this.selection.borrow().contains(&index));
check.connect_toggled(clone!(@strong this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
}));
let row = libhandy::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_title(Some(&part.title));
row.show_all();
parts_list.add(&row);
}
this
}
/// Set the closure to be called when the user has edited the track.
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for TrackEditor {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,122 @@
use super::disc_source::DiscSource;
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
/// A screen for selecting tracks from a medium.
pub struct TrackSelector {
source: Rc<DiscSource>,
widget: gtk::Box,
select_button: gtk::Button,
selection: RefCell<Vec<usize>>,
selected_cb: RefCell<Option<Box<dyn Fn(Vec<usize>)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackSelector {
/// Create a new track selector.
pub fn new(source: Rc<DiscSource>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, select_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = gtk::ListBox::new();
track_list.set_selection_mode(gtk::SelectionMode::None);
track_list.set_vexpand(false);
track_list.show();
tracks_frame.add(&track_list);
let this = Rc::new(Self {
source,
widget,
select_button,
selection: RefCell::new(Vec::new()),
selected_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.select_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
if let Some(cb) = &*this.selected_cb.borrow() {
let selection = this.selection.borrow().clone();
cb(selection);
}
}));
for (index, track) in this.source.tracks.iter().enumerate() {
let check = gtk::CheckButton::new();
check.connect_toggled(clone!(@strong this => move |check| {
let mut selection = this.selection.borrow_mut();
if check.get_active() {
selection.push(index);
} else {
if let Some(pos) = selection.iter().position(|part| *part == index) {
selection.remove(pos);
}
}
if selection.is_empty() {
this.select_button.set_sensitive(false);
} else {
this.select_button.set_sensitive(true);
}
}));
let title = format!("Track {}", track.number);
let row = libhandy::ActionRow::new();
row.add_prefix(&check);
row.set_activatable_widget(Some(&check));
row.set_title(Some(&title));
row.show_all();
track_list.add(&row);
}
this
}
/// Set the closure to be called when the user has selected tracks. The
/// closure will be called with the indices of the selected tracks as its
/// argument.
pub fn set_selected_cb<F: Fn(Vec<usize>) + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for TrackSelector {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,283 @@
use super::disc_source::DiscSource;
use super::track_editor::TrackEditor;
use super::track_selector::TrackSelector;
use crate::backend::Backend;
use crate::database::{Recording, Track, TrackSet};
use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector};
use crate::widgets::{Navigator, NavigatorScreen};
use crate::widgets::new_list::List;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
/// A track set before being imported.
#[derive(Clone, Debug)]
pub struct TrackSetData {
pub recording: Recording,
pub tracks: Vec<TrackData>,
}
/// A track before being imported.
#[derive(Clone, Debug)]
pub struct TrackData {
/// Index of the track source within the medium source's tracks.
pub track_source: usize,
/// Actual track data.
pub work_parts: Vec<usize>,
}
/// A screen for editing a set of tracks for one recording.
pub struct TrackSetEditor {
backend: Rc<Backend>,
source: Rc<DiscSource>,
widget: gtk::Box,
save_button: gtk::Button,
recording_row: libhandy::ActionRow,
track_list: List,
recording: RefCell<Option<Recording>>,
tracks: RefCell<Vec<TrackData>>,
done_cb: RefCell<Option<Box<dyn Fn(TrackSetData)>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl TrackSetEditor {
/// Create a new track set editor.
pub fn new(backend: Rc<Backend>, source: Rc<DiscSource>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, libhandy::ActionRow, recording_row);
get_widget!(builder, gtk::Button, select_recording_button);
get_widget!(builder, gtk::Button, edit_tracks_button);
get_widget!(builder, gtk::Frame, tracks_frame);
let track_list = List::new(&gettext!("No tracks added"));
tracks_frame.add(&track_list.widget);
let this = Rc::new(Self {
backend,
source,
widget,
save_button,
recording_row,
track_list,
recording: RefCell::new(None),
tracks: RefCell::new(Vec::new()),
done_cb: RefCell::new(None),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
this.save_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.done_cb.borrow() {
let data = TrackSetData {
recording: this.recording.borrow().clone().unwrap(),
tracks: this.tracks.borrow().clone(),
};
cb(data);
}
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
select_recording_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let person_selector = PersonSelector::new(this.backend.clone());
person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| {
let work_selector = WorkSelector::new(this.backend.clone(), person.clone());
work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| {
let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone());
recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| {
this.recording.replace(Some(recording.clone()));
this.recording_selected();
navigator.clone().pop();
navigator.clone().pop();
navigator.clone().pop();
}));
navigator.clone().push(recording_selector);
}));
navigator.clone().push(work_selector);
}));
navigator.clone().push(person_selector);
}
}));
edit_tracks_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let selector = TrackSelector::new(Rc::clone(&this.source));
selector.set_selected_cb(clone!(@strong this => move |selection| {
let mut tracks = Vec::new();
for index in selection {
let data = TrackData {
track_source: index,
work_parts: Vec::new(),
};
tracks.push(data);
}
let length = tracks.len();
this.tracks.replace(tracks);
this.track_list.update(length);
this.autofill_parts();
}));
navigator.push(selector);
}
}));
this.track_list.set_make_widget(clone!(@strong this => move |index| {
let track = &this.tracks.borrow()[index];
let mut title_parts = Vec::<String>::new();
if let Some(recording) = &*this.recording.borrow() {
for part in &track.work_parts {
title_parts.push(recording.work.parts[*part].title.clone());
}
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let number = this.source.tracks[track.track_source].number;
let subtitle = format!("Track {}", number);
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button);
let edit_button = gtk::Button::new();
edit_button.set_relief(gtk::ReliefStyle::None);
edit_button.set_valign(gtk::Align::Center);
edit_button.add(&edit_image);
let row = libhandy::ActionRow::new();
row.set_activatable(true);
row.set_title(Some(&title));
row.set_subtitle(Some(&subtitle));
row.add(&edit_button);
row.set_activatable_widget(Some(&edit_button));
row.show_all();
edit_button.connect_clicked(clone!(@strong this => move |_| {
let recording = this.recording.borrow().clone();
let navigator = this.navigator.borrow().clone();
if let (Some(recording), Some(navigator)) = (recording, navigator) {
let track = &this.tracks.borrow()[index];
let editor = TrackEditor::new(recording, track.work_parts.clone());
editor.set_selected_cb(clone!(@strong this => move |selection| {
{
let mut tracks = this.tracks.borrow_mut();
let mut track = &mut tracks[index];
track.work_parts = selection;
};
this.update_tracks();
}));
navigator.push(editor);
}
}));
row.upcast()
}));
this
}
/// Set the closure to be called when the user has created the track set.
pub fn set_done_cb<F: Fn(TrackSetData) + 'static>(&self, cb: F) {
self.done_cb.replace(Some(Box::new(cb)));
}
/// Set everything up after selecting a recording.
fn recording_selected(&self) {
if let Some(recording) = &*self.recording.borrow() {
self.recording_row.set_title(Some(&recording.work.get_title()));
self.recording_row.set_subtitle(Some(&recording.get_performers()));
self.save_button.set_sensitive(true);
}
self.autofill_parts();
}
/// Automatically try to put work part information from the selected recording into the
/// selected tracks.
fn autofill_parts(&self) {
if let Some(recording) = &*self.recording.borrow() {
let mut tracks = self.tracks.borrow_mut();
for (index, _) in recording.work.parts.iter().enumerate() {
if let Some(mut track) = tracks.get_mut(index) {
track.work_parts = vec![index];
} else {
break;
}
}
}
self.update_tracks();
}
/// Update the track list.
fn update_tracks(&self) {
let length = self.tracks.borrow().len();
self.track_list.update(length);
}
}
impl NavigatorScreen for TrackSetEditor {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -16,6 +16,7 @@ mod config;
mod database; mod database;
mod dialogs; mod dialogs;
mod editors; mod editors;
mod import;
mod player; mod player;
mod screens; mod screens;
mod selectors; mod selectors;
@ -31,6 +32,7 @@ fn main() {
gettextrs::bindtextdomain("musicus", config::LOCALEDIR); gettextrs::bindtextdomain("musicus", config::LOCALEDIR);
gettextrs::textdomain("musicus"); gettextrs::textdomain("musicus");
gstreamer::init().expect("Failed to initialize GStreamer!");
gtk::init().expect("Failed to initialize GTK!"); gtk::init().expect("Failed to initialize GTK!");
libhandy::init(); libhandy::init();
resources::init().expect("Failed to initialize resources!"); resources::init().expect("Failed to initialize resources!");

View file

@ -33,9 +33,9 @@ run_command(
) )
sources = files( sources = files(
'backend/client/mod.rs',
'backend/client/ensembles.rs', 'backend/client/ensembles.rs',
'backend/client/instruments.rs', 'backend/client/instruments.rs',
'backend/client/mod.rs',
'backend/client/persons.rs', 'backend/client/persons.rs',
'backend/client/recordings.rs', 'backend/client/recordings.rs',
'backend/client/works.rs', 'backend/client/works.rs',
@ -44,12 +44,12 @@ sources = files(
'backend/secure.rs', 'backend/secure.rs',
'database/ensembles.rs', 'database/ensembles.rs',
'database/instruments.rs', 'database/instruments.rs',
'database/medium.rs',
'database/mod.rs', 'database/mod.rs',
'database/persons.rs', 'database/persons.rs',
'database/recordings.rs', 'database/recordings.rs',
'database/schema.rs', 'database/schema.rs',
'database/thread.rs', 'database/thread.rs',
'database/tracks.rs',
'database/works.rs', 'database/works.rs',
'dialogs/about.rs', 'dialogs/about.rs',
'dialogs/login_dialog.rs', 'dialogs/login_dialog.rs',
@ -62,8 +62,6 @@ sources = files(
'editors/performance.rs', 'editors/performance.rs',
'editors/person.rs', 'editors/person.rs',
'editors/recording.rs', 'editors/recording.rs',
'editors/track.rs',
'editors/tracks.rs',
'editors/work.rs', 'editors/work.rs',
'editors/work_part.rs', 'editors/work_part.rs',
'editors/work_section.rs', 'editors/work_section.rs',

View file

@ -8,8 +8,8 @@ use std::rc::Rc;
#[derive(Clone)] #[derive(Clone)]
pub struct PlaylistItem { pub struct PlaylistItem {
pub recording: Recording, pub track_set: TrackSet,
pub tracks: Vec<Track>, pub indices: Vec<usize>,
} }
pub struct Player { pub struct Player {
@ -19,11 +19,11 @@ pub struct Player {
current_item: Cell<Option<usize>>, current_item: Cell<Option<usize>>,
current_track: Cell<Option<usize>>, current_track: Cell<Option<usize>>,
playing: Cell<bool>, playing: Cell<bool>,
playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>) -> ()>>>, playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>,
track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize) -> ()>>>, track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>,
duration_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
playing_cbs: RefCell<Vec<Box<dyn Fn(bool) -> ()>>>, playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>,
position_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>,
} }
impl Player { impl Player {
@ -80,23 +80,23 @@ impl Player {
result result
} }
pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) -> () + 'static>(&self, cb: F) { pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) {
self.playlist_cbs.borrow_mut().push(Box::new(cb)); self.playlist_cbs.borrow_mut().push(Box::new(cb));
} }
pub fn add_track_cb<F: Fn(usize, usize) -> () + 'static>(&self, cb: F) { pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) {
self.track_cbs.borrow_mut().push(Box::new(cb)); self.track_cbs.borrow_mut().push(Box::new(cb));
} }
pub fn add_duration_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) { pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.duration_cbs.borrow_mut().push(Box::new(cb)); self.duration_cbs.borrow_mut().push(Box::new(cb));
} }
pub fn add_playing_cb<F: Fn(bool) -> () + 'static>(&self, cb: F) { pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) {
self.playing_cbs.borrow_mut().push(Box::new(cb)); self.playing_cbs.borrow_mut().push(Box::new(cb));
} }
pub fn add_position_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) { pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) {
self.position_cbs.borrow_mut().push(Box::new(cb)); self.position_cbs.borrow_mut().push(Box::new(cb));
} }
@ -121,7 +121,7 @@ impl Player {
} }
pub fn add_item(&self, item: PlaylistItem) -> Result<()> { pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
if item.tracks.is_empty() { if item.indices.is_empty() {
Err(anyhow!( Err(anyhow!(
"Tried to add playlist item without tracks to playlist!" "Tried to add playlist item without tracks to playlist!"
)) ))
@ -199,7 +199,7 @@ impl Player {
current_track -= 1; current_track -= 1;
} else if current_item > 0 { } else if current_item > 0 {
current_item -= 1; current_item -= 1;
current_track = playlist[current_item].tracks.len() - 1; current_track = playlist[current_item].indices.len() - 1;
} else { } else {
return Err(anyhow!("No previous track!")); return Err(anyhow!("No previous track!"));
} }
@ -213,7 +213,7 @@ impl Player {
let playlist = self.playlist.borrow(); let playlist = self.playlist.borrow();
let item = &playlist[current_item]; let item = &playlist[current_item];
current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len() current_track + 1 < item.indices.len() || current_item + 1 < playlist.len()
} else { } else {
false false
} }
@ -231,7 +231,7 @@ impl Player {
let playlist = self.playlist.borrow(); let playlist = self.playlist.borrow();
let item = &playlist[current_item]; let item = &playlist[current_item];
if current_track + 1 < item.tracks.len() { if current_track + 1 < item.indices.len() {
current_track += 1; current_track += 1;
} else if current_item + 1 < playlist.len() { } else if current_item + 1 < playlist.len() {
current_item += 1; current_item += 1;
@ -248,15 +248,7 @@ impl Player {
"file://{}", "file://{}",
self.music_library_path self.music_library_path
.join( .join(
self.playlist self.playlist.borrow()[current_item].track_set.tracks[current_track].path.clone(),
.borrow()
.get(current_item)
.ok_or(anyhow!("Playlist item doesn't exist!"))?
.tracks
.get(current_track)
.ok_or(anyhow!("Track doesn't exist!"))?
.file_name
.clone(),
) )
.to_str() .to_str()
.unwrap(), .unwrap(),

View file

@ -215,15 +215,17 @@ impl PlayerScreen {
elements.push(PlaylistElement { elements.push(PlaylistElement {
item: item_index, item: item_index,
track: 0, track: 0,
title: item.recording.work.get_title(), title: item.track_set.recording.work.get_title(),
subtitle: Some(item.recording.get_performers()), subtitle: Some(item.track_set.recording.get_performers()),
playable: false, playable: false,
}); });
for (track_index, track) in item.tracks.iter().enumerate() { for track_index in &item.indices {
let track = &item.track_set.tracks[*track_index];
let mut parts = Vec::<String>::new(); let mut parts = Vec::<String>::new();
for part in &track.work_parts { for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone()); parts.push(item.track_set.recording.work.parts[*part].title.clone());
} }
let title = if parts.is_empty() { let title = if parts.is_empty() {
@ -234,7 +236,7 @@ impl PlayerScreen {
elements.push(PlaylistElement { elements.push(PlaylistElement {
item: item_index, item: item_index,
track: track_index, track: *track_index,
title: title, title: title,
subtitle: None, subtitle: None,
playable: true, playable: true,
@ -262,20 +264,20 @@ impl PlayerScreen {
next_button.set_sensitive(player.has_next()); next_button.set_sensitive(player.has_next());
let item = &playlist.borrow()[current_item]; let item = &playlist.borrow()[current_item];
let track = &item.tracks[current_track]; let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new(); let mut parts = Vec::<String>::new();
for part in &track.work_parts { for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone()); parts.push(item.track_set.recording.work.parts[*part].title.clone());
} }
let mut title = item.recording.work.get_title(); let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() { if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", ")); title = format!("{}: {}", title, parts.join(", "));
} }
title_label.set_text(&title); title_label.set_text(&title);
subtitle_label.set_text(&item.recording.get_performers()); subtitle_label.set_text(&item.track_set.recording.get_performers());
position_label.set_text("0:00"); position_label.set_text("0:00");
self_item.replace(current_item); self_item.replace(current_item);

View file

@ -1,6 +1,6 @@
use crate::backend::*; use crate::backend::*;
use crate::database::*; use crate::database::*;
use crate::editors::{RecordingEditor, TracksEditor}; use crate::editors::RecordingEditor;
use crate::player::*; use crate::player::*;
use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow};
use gettextrs::gettext; use gettextrs::gettext;
@ -17,7 +17,7 @@ pub struct RecordingScreen {
recording: Recording, recording: Recording,
widget: gtk::Box, widget: gtk::Box,
stack: gtk::Stack, stack: gtk::Stack,
tracks: RefCell<Vec<Track>>, track_sets: RefCell<Vec<TrackSet>>,
navigator: RefCell<Option<Rc<Navigator>>>, navigator: RefCell<Option<Rc<Navigator>>>,
} }
@ -56,35 +56,43 @@ impl RecordingScreen {
recording, recording,
widget, widget,
stack, stack,
tracks: RefCell::new(Vec::new()), track_sets: RefCell::new(Vec::new()),
navigator: RefCell::new(None), navigator: RefCell::new(None),
}); });
list.set_make_widget(clone!(@strong result => move |track: &Track| { list.set_make_widget(clone!(@strong result => move |track_set: &TrackSet| {
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
title_parts.push(result.recording.work.parts[*part].title.clone());
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let title_label = gtk::Label::new(Some(&title));
title_label.set_ellipsize(pango::EllipsizeMode::End);
title_label.set_halign(gtk::Align::Start);
let file_name_label = gtk::Label::new(Some(&track.file_name));
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
file_name_label.set_opacity(0.5);
file_name_label.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.set_border_width(6); vbox.set_border_width(6);
vbox.add(&title_label); vbox.set_spacing(6);
vbox.add(&file_name_label);
for track in &track_set.tracks {
let track_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
let mut title_parts = Vec::<String>::new();
for part in &track.work_parts {
title_parts.push(result.recording.work.parts[*part].title.clone());
}
let title = if title_parts.is_empty() {
gettext("Unknown")
} else {
title_parts.join(", ")
};
let title_label = gtk::Label::new(Some(&title));
title_label.set_ellipsize(pango::EllipsizeMode::End);
title_label.set_halign(gtk::Align::Start);
let file_name_label = gtk::Label::new(Some(&track.path));
file_name_label.set_ellipsize(pango::EllipsizeMode::End);
file_name_label.set_opacity(0.5);
file_name_label.set_halign(gtk::Align::Start);
track_box.add(&title_label);
track_box.add(&file_name_label);
vbox.add(&track_box);
}
vbox.upcast() vbox.upcast()
})); }));
@ -97,12 +105,12 @@ impl RecordingScreen {
})); }));
add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| { add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| {
if let Some(player) = result.backend.get_player() { // if let Some(player) = result.backend.get_player() {
player.add_item(PlaylistItem { // player.add_item(PlaylistItem {
recording: result.recording.clone(), // track_set: result.track_sets.get(0).unwrap().clone(),
tracks: result.tracks.borrow().clone(), // indices: result.tracks.borrow().clone(),
}).unwrap(); // }).unwrap();
} // }
})); }));
edit_action.connect_activate(clone!(@strong result => move |_, _| { edit_action.connect_activate(clone!(@strong result => move |_, _| {
@ -121,33 +129,33 @@ impl RecordingScreen {
})); }));
edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| { edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); // let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone());
let window = NavigatorWindow::new(editor); // let window = NavigatorWindow::new(editor);
window.show(); // window.show();
})); }));
delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| { delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| {
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = result.clone(); let clone = result.clone();
context.spawn_local(async move { context.spawn_local(async move {
clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); // clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap();
clone.backend.library_changed(); // clone.backend.library_changed();
}); });
})); }));
let context = glib::MainContext::default(); let context = glib::MainContext::default();
let clone = result.clone(); let clone = result.clone();
context.spawn_local(async move { context.spawn_local(async move {
let tracks = clone let track_sets = clone
.backend .backend
.db() .db()
.get_tracks(&clone.recording.id) .get_track_sets(&clone.recording.id)
.await .await
.unwrap(); .unwrap();
list.show_items(tracks.clone()); list.show_items(track_sets.clone());
clone.stack.set_visible_child_name("content"); clone.stack.set_visible_child_name("content");
clone.tracks.replace(tracks); clone.track_sets.replace(track_sets);
}); });
result result

View file

@ -63,6 +63,16 @@ where
this this
} }
pub fn set_selectable(&self, selectable: bool) {
let mode = if selectable {
gtk::SelectionMode::Single
} else {
gtk::SelectionMode::None
};
self.widget.set_selection_mode(mode);
}
pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) { pub fn set_make_widget<F: Fn(&T) -> gtk::Widget + 'static>(&self, make_widget: F) {
self.make_widget.replace(Some(Box::new(make_widget))); self.make_widget.replace(Some(Box::new(make_widget)));
} }

View file

@ -7,6 +7,8 @@ pub use navigator::*;
pub mod navigator_window; pub mod navigator_window;
pub use navigator_window::*; pub use navigator_window::*;
pub mod new_list;
pub mod player_bar; pub mod player_bar;
pub use player_bar::*; pub use player_bar::*;

View file

@ -0,0 +1,50 @@
use gtk::prelude::*;
use std::cell::RefCell;
/// A simple list of widgets.
pub struct List {
pub widget: gtk::ListBox,
make_widget: RefCell<Option<Box<dyn Fn(usize) -> gtk::Widget>>>,
}
impl List {
/// Create a new list. The list will be empty.
pub fn new(placeholder_text: &str) -> Self {
let placeholder_label = gtk::Label::new(Some(placeholder_text));
placeholder_label.set_margin_top(6);
placeholder_label.set_margin_bottom(6);
placeholder_label.set_margin_start(6);
placeholder_label.set_margin_end(6);
placeholder_label.show();
let widget = gtk::ListBox::new();
widget.set_selection_mode(gtk::SelectionMode::None);
widget.set_placeholder(Some(&placeholder_label));
widget.show();
Self {
widget,
make_widget: RefCell::new(None),
}
}
/// Set the closure to be called to construct widgets for the items.
pub fn set_make_widget<F: Fn(usize) -> gtk::Widget + 'static>(&self, make_widget: F) {
self.make_widget.replace(Some(Box::new(make_widget)));
}
/// Call the make_widget function for each item. This will automatically
/// show all children by indices 0..length.
pub fn update(&self, length: usize) {
for child in self.widget.get_children() {
self.widget.remove(&child);
}
if let Some(make_widget) = &*self.make_widget.borrow() {
for index in 0..length {
let row = make_widget(index);
self.widget.insert(&row, -1);
}
}
}
}

View file

@ -112,20 +112,20 @@ impl PlayerBar {
next_button.set_sensitive(player.has_next()); next_button.set_sensitive(player.has_next());
let item = &playlist.borrow()[current_item]; let item = &playlist.borrow()[current_item];
let track = &item.tracks[current_track]; let track = &item.track_set.tracks[current_track];
let mut parts = Vec::<String>::new(); let mut parts = Vec::<String>::new();
for part in &track.work_parts { for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone()); parts.push(item.track_set.recording.work.parts[*part].title.clone());
} }
let mut title = item.recording.work.get_title(); let mut title = item.track_set.recording.work.get_title();
if !parts.is_empty() { if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", ")); title = format!("{}: {}", title, parts.join(", "));
} }
title_label.set_text(&title); title_label.set_text(&title);
subtitle_label.set_text(&item.recording.get_performers()); subtitle_label.set_text(&item.track_set.recording.get_performers());
position_label.set_text("0:00"); position_label.set_text("0:00");
} }
)); ));

View file

@ -1,6 +1,6 @@
use crate::backend::*; use crate::backend::*;
use crate::dialogs::*; use crate::dialogs::*;
use crate::editors::TracksEditor; use crate::import::SourceSelector;
use crate::screens::*; use crate::screens::*;
use crate::widgets::*; use crate::widgets::*;
use futures::prelude::*; use futures::prelude::*;
@ -85,13 +85,17 @@ impl Window {
})); }));
add_button.connect_clicked(clone!(@strong result => move |_| { add_button.connect_clicked(clone!(@strong result => move |_| {
let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); // let editor = TracksEditor::new(result.backend.clone(), None, Vec::new());
editor.set_callback(clone!(@strong result => move || { // editor.set_callback(clone!(@strong result => move || {
result.reload(); // result.reload();
})); // }));
let window = NavigatorWindow::new(editor); // let window = NavigatorWindow::new(editor);
// window.show();
let dialog = SourceSelector::new(result.backend.clone());
let window = NavigatorWindow::new(dialog);
window.show(); window.show();
})); }));
@ -107,6 +111,16 @@ impl Window {
result.stack.set_visible_child_name("content"); result.stack.set_visible_child_name("content");
})); }));
// action!(
// result.window,
// "import-disc",
// clone!(@strong result => move |_, _| {
// let dialog = ImportDiscDialog::new(result.backend.clone());
// let window = NavigatorWindow::new(dialog);
// window.show();
// })
// );
action!( action!(
result.window, result.window,
"preferences", "preferences",