Skip to content

Commit

Permalink
Initial commit (force-pushed)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaDoes committed Jul 11, 2023
0 parents commit bd160cf
Show file tree
Hide file tree
Showing 42 changed files with 5,599 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Configuration
config*.json

# Cache
cache/
*.blob

# Output executables
*.exe
*.app
*.old
*.bak

# Most probable output executables for Linux
libremedia.app

# Log files
*.log

# Supervise
run
supervise/

# OS leftovers
desktop.ini
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
### libremedia progress tracker before release

# User interface

- Fetch artwork for an object using `:artwork` URI extension instead of re-fetching the entire source object, use `:1280` to smartly limit size to 1280px or next size up for max quality
- Add visibility toggle buttons to both the search bar and the audio player
- Increase size of controls in tables to maximize room for touch screens, OR migrate all controls to drop-down list, whichever works out better (or condense controls to it if table entry is squished?)

## Pages

- Display top 100 streams and top 100 downloads on home page
- Add "download album" control on album page, generates and saves single-folder ZIP of all streams client-side with per-stream progress bar
- Add "download discography" control on creator page, generates and saves multi-folder ZIP of all albums, EPs and singles client-side with per-album and per-stream progress bars
- Add playlists section on search and creator pages, uses same handler for album objects
- Display entry numbers and total X of each table section

- Create queue management using /queue with no params
* Generic streams table, only shows either the regular queue or the shuffled queue
* Add reorder control, injects clickable dividers in-between streams that will move selected stream to that location
* Add unqueue control, removes stream from queue
* Allow saving current queue as a libremedia playlist

- Rewrite logic for transcript page
* Fetch transcript with timings using `:transcript` URI extension instead of re-fetching entire stream object
* Start audio position 0 with empty line (unless first line begins at pos 0) using timings index 0 instead of fake -1 like before (invalid index)
* Signal unplayed/restarted auto-scroller position tracker using -1 instead of -2 like before
* Always load now playing transcript into timings buffer when stream changes, automaticlly scroll back to top
* Add fixed resume auto-scroll button to bottom right when user scrolls to disable auto-scroll

## Audio player

- Add vertical volume slider
- Add horizontal audio position seeker directly above audio player, show regardless of audio player visibility
- Change transcript button to specify no params instead of pointing to now playing stream URI, no params will sync transcript with now playing
- Display smaller album art somewhere, use either same size or next size up for max quality

- Add shuffled queue support
* Place shuffle button somewhere
* Generate new shuffled queue using existing queue on every enable, and use it for next/prev handling until disable
* On disable, return to original queue regardless of what was next prior to enable

# Backend

- Start anonymously tracking listen and download counts
- At end of object expansion goroutine, spawn new goroutine to search other providers for matching object to fill in missing metadata (such as credited creators, biographies, artwork, albums, streams, etc)
- Migrate all client-side player controls to the server, simulating client actions based on client requests
- Require clients to request to start playback (automatically acting as "I'm ready" for shared sessions to minimize latency), so they always load from `/v1/stream` with no params afterward
- Add `/v1/providers` endpoint to return all upstream providers and a path to retrieve their icons, which can follow upstream to the source provider
- Add providers array param to `/v1/search` endpoint, allows client-side filtering of providers via request (useful to minimize processing and deduplicate responses when a provider is shared across upstream instances)
- Convert transcript handler to be separated transcript providers, also available as plugins
- Allow catalogue and database providers to be implemented as multimedia providers, without the streams
- Implement support for ffmpeg (for custom format, codec, and quality params, plus metadata injection with `/v1/download` endpoint)
- Implement support for ffprobe (pointing to internal `/v1/stream` API) to identify format details if not available on provider, but direct stream is available
- Find a reliable and free way to identify audio streams with no metadata

## Plugins

- Finish writing new plugin system, simple concept but a lot to explain, later
- Implement the Rust librespot client as a binary plugin to provide Spotify, and add podcast support
- Remove hardcoded Spotify provider
- Migrate hardcoded Tidal provider to a binary plugin, and add music video support
- Create plugins for many other things, like local content, disc drive / ISO support, torrent support, YouTube, Bandcamp, SoundCloud, Apple Music, Deezer, etc

## User accounts

- Provide a default guest user using server config
- Provide a default admin user with permission to manage additional users and control the quality settings, globally and of each user

- Cache now playing stream to disk as it buffers (can be random access too)
* Hold lock on file until all sessions holding lock either timeout or all choose new streams
* Avoid repeated reads with session sharing by using the same buffer, as all sessions are synced

- Add session sharing support
* Generate an invite link to share with any user
* Sync queue, now playing, and audio position in real time (with both a low-latency and data saver mode client-side)
* If sharer toggles it, users may vote to skip with >50% majority
* If sharer toggles it, users may append to queue (with a rotational selection mode in client join order, and a free for all mode)
149 changes: 149 additions & 0 deletions builders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"encoding/json"
"fmt"
"strings"
)

// GetObject returns an object, either from the cache, or live if possible
func GetObject(uri string) (obj *Object) {
//Try the cache first, it will update frequently during a live expansion
obj = GetObjectCached(uri)
if obj != nil {
return obj
}

//Fetch the object live
obj = GetObjectLive(uri)
if obj == nil {
return
}

obj.Sync()
return
}

// GetObjectLive returns a live object from a given URI
func GetObjectLive(mediaURI string) (obj *Object) {
if mediaURI == "" {
Error.Println("Cannot get object with empty mediaURI")
return nil
}

obj = &Object{URI: mediaURI, Object: &json.RawMessage{}}
Trace.Println("Fetching " + mediaURI + " live")

splitURI := strings.Split(mediaURI, ":")
switch splitURI[0] {
case "bestmatch": //Returns an object that best matches the given search query
if len(splitURI) < 2 {
return NewObjError("bestmatch: need query")
}
query := strings.ReplaceAll(splitURI[1], "+", " ")
query = strings.ReplaceAll(query, "%20", " ")
query = strings.ToLower(query)
searchResultsObj := GetObjectLive("search:" + splitURI[1])
if searchResultsObj.Type != "search" {
return searchResultsObj
}
searchResults := searchResultsObj.SearchResults()
if len(searchResults.Streams) > 0 {
return GetObjectLive(searchResults.Streams[0].Stream().URI)
}
if len(searchResults.Creators) > 0 {
return GetObjectLive(searchResults.Creators[0].Creator().URI)
}
if len(searchResults.Albums) > 0 {
return GetObjectLive(searchResults.Albums[0].Album().URI)
}
return NewObjError("bestmatch: try a better query")
case "search": //Main search handler
if len(splitURI) < 2 {
return NewObjError("search: need query")
}
query := strings.ReplaceAll(splitURI[1], "+", " ")
query = strings.ReplaceAll(query, "%20", " ")
query = strings.ToLower(query)
obj.URI = "search:" + query
results := &ObjectSearchResults{Query: query}
for i := 0; i < len(providers); i++ {
Trace.Println("Searching for '" + query + "' on " + providers[i])
handler := handlers[providers[i]]
res, err := handler.Search(query)
if err != nil {
Error.Printf("Error searching on %s: %v", providers[i], err)
continue
}
if len(res.Creators) > 0 {
results.Creators = append(results.Creators, res.Creators...)
}
if len(res.Albums) > 0 {
results.Albums = append(results.Albums, res.Albums...)
}
if len(res.Streams) > 0 {
results.Streams = append(results.Streams, res.Streams...)
}
}
obj.Type = "search"
obj.Provider = "libremedia"
resultsJSON, err := json.Marshal(results)
if err != nil {
Error.Printf("Unable to marshal search results: %v\n", err)
return
}

obj.Object.UnmarshalJSON(resultsJSON)
return
}

if handler, exists := handlers[splitURI[0]]; exists {
obj.Provider = splitURI[0]
if len(splitURI) > 2 {
id := splitURI[2]
switch splitURI[1] {
case "artist", "creator", "user", "channel", "chan", "streamer":
creator, err := handler.Creator(id)
if err != nil {
return NewObjError(fmt.Sprintf("invalid creator %s: %v", id, err))
}
obj.Type = "creator"
creatorJSON, err := json.Marshal(creator)
if err != nil {
Error.Printf("Unable to marshal creator: %v\n", err)
return
}
obj.Object.UnmarshalJSON(creatorJSON)
case "album":
album, err := handler.Album(id)
if err != nil {
return NewObjError(fmt.Sprintf("invalid album %s: %v", id, err))
}
obj.Type = "album"
albumJSON, err := json.Marshal(album)
if err != nil {
Error.Printf("Unable to marshal album: %v\n", err)
return
}
obj.Object.UnmarshalJSON(albumJSON)
case "track", "song", "video", "audio", "stream":
stream, err := handler.Stream(id)
if err != nil {
return NewObjError(fmt.Sprintf("invalid stream %s: %v", id, err))
}
obj.Type = "stream"
stream.Transcribe()
streamJSON, err := json.Marshal(stream)
if err != nil {
Error.Printf("Unable to marshal stream: %v\n", err)
return
}
obj.Object.UnmarshalJSON(streamJSON)
}

return obj
}
}

return nil
}
23 changes: 23 additions & 0 deletions css/audioplayer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#audioControls button {
margin: 15px;
font-size: 2em;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
cursor: crosshair;
text-align: center;

padding: 0;
border: none;
background: none;
color: white;
background-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}

#audioControls button:focus {
outline: none;
}
Loading

0 comments on commit bd160cf

Please sign in to comment.