Add initial draft for core
First attempt for common types and the graphql schema
This commit is contained in:
commit
91e988f722
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
[workspace]
|
||||||
|
members = ["crabidy-core"]
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "crabidy-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
futures = "0.3.28"
|
||||||
|
graphql_client = "0.12.0"
|
||||||
|
juniper = { git = "https://github.com/graphql-rust/juniper.git", branch = "master" }
|
||||||
|
juniper_subscriptions = { git = "https://github.com/graphql-rust/juniper.git", branch = "master" }
|
||||||
|
serde = "1.0.163"
|
||||||
|
# juniper = { version = "0.16.0", features = ["serde_json"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
futures = "0.3.28"
|
||||||
|
graphql_client = "0.12.0"
|
||||||
|
juniper = { git = "https://github.com/graphql-rust/juniper.git", branch = "master" }
|
||||||
|
juniper_subscriptions = { git = "https://github.com/graphql-rust/juniper.git", branch = "master" }
|
||||||
|
serde = "1.0.163"
|
||||||
|
# juniper = { version = "0.16.0", features = ["serde_json"] }
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
use juniper::{EmptyMutation, EmptySubscription, IntrospectionFormat};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
#[path = "src/lib.rs"]
|
||||||
|
mod lib;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// let (res, _errors) = juniper::introspect(
|
||||||
|
// &lib::Schema::new(
|
||||||
|
// lib::ItemList::new(),
|
||||||
|
// lib::Queue::new(),
|
||||||
|
// lib::Subscription::new(),
|
||||||
|
// ),
|
||||||
|
// &(),
|
||||||
|
// IntrospectionFormat::default(),
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// let mut file = File::create("src/schema.json").unwrap();
|
||||||
|
// let json_result = serde_json::to_string_pretty(&res).unwrap();
|
||||||
|
// file.write_all(json_result.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let schema = lib::Schema::new(
|
||||||
|
lib::ItemList::new(),
|
||||||
|
lib::Mutation::new(),
|
||||||
|
lib::Subscription::new(),
|
||||||
|
);
|
||||||
|
let schema_str = schema.as_schema_language();
|
||||||
|
|
||||||
|
let mut file = File::create("src/schema.graphql").unwrap();
|
||||||
|
file.write_all(schema_str.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::Stream;
|
||||||
|
use juniper::{
|
||||||
|
graphql_object, graphql_subscription, FieldError, FieldResult, GraphQLEnum, GraphQLInputObject,
|
||||||
|
GraphQLObject, RootNode,
|
||||||
|
};
|
||||||
|
use std::{collections::HashMap, pin::Pin, sync::Arc};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ProviderClient: Send + Sync {
|
||||||
|
async fn get_urls_for_track(&self, track_uuid: &str) -> Result<Vec<String>, ProviderError>;
|
||||||
|
async fn get_item_lists_full(&self) -> Result<Vec<ItemList>, ProviderError>;
|
||||||
|
async fn get_item_lists_partial(
|
||||||
|
&self,
|
||||||
|
list_uuid: String,
|
||||||
|
) -> Result<Vec<ItemList>, ProviderError>;
|
||||||
|
async fn get_item_list_partial_without_children(
|
||||||
|
&self,
|
||||||
|
list_uuid: String,
|
||||||
|
) -> Result<ItemList, ProviderError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash)]
|
||||||
|
pub enum ProviderError {
|
||||||
|
UnknownUser,
|
||||||
|
FetchError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ItemList {
|
||||||
|
pub name: String,
|
||||||
|
pub uuid: String,
|
||||||
|
pub provider: String,
|
||||||
|
pub tracks: Option<Vec<Track>>,
|
||||||
|
pub children: Option<Vec<ItemList>>,
|
||||||
|
pub is_queable: bool,
|
||||||
|
without_children: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl ItemList {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn uuid(&self) -> &str {
|
||||||
|
&self.uuid
|
||||||
|
}
|
||||||
|
fn provider(&self) -> &str {
|
||||||
|
&self.provider
|
||||||
|
}
|
||||||
|
//TODO: Be smarter than clone here
|
||||||
|
fn tracks(&self, refresh: bool) -> Option<Vec<Track>> {
|
||||||
|
self.tracks.clone()
|
||||||
|
}
|
||||||
|
//TODO: Be smarter than clone here
|
||||||
|
fn children(&self, refresh: bool) -> Option<Vec<ItemList>> {
|
||||||
|
self.children.clone()
|
||||||
|
}
|
||||||
|
fn is_queable(&self) -> bool {
|
||||||
|
self.is_queable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemList {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "root".to_string(),
|
||||||
|
uuid: "root".to_string(),
|
||||||
|
provider: "root".to_string(),
|
||||||
|
tracks: None,
|
||||||
|
children: None,
|
||||||
|
is_queable: false,
|
||||||
|
without_children: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn flatten(&self) -> Vec<Track> {
|
||||||
|
let mut tracks = Vec::new();
|
||||||
|
if let Some(own_tracks) = &self.tracks {
|
||||||
|
tracks.extend(own_tracks.clone());
|
||||||
|
}
|
||||||
|
if let Some(childs) = &self.children {
|
||||||
|
for child in childs {
|
||||||
|
tracks.extend(child.flatten());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLEnum)]
|
||||||
|
enum PlayState {
|
||||||
|
Buffering,
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLObject)]
|
||||||
|
pub struct Track {
|
||||||
|
pub title: String,
|
||||||
|
pub uuid: String,
|
||||||
|
pub duration: Option<i32>,
|
||||||
|
pub album: Option<Album>,
|
||||||
|
pub artist: Option<Artist>,
|
||||||
|
pub provider: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLInputObject)]
|
||||||
|
pub struct InputTrack {
|
||||||
|
pub uuid: String,
|
||||||
|
pub provider: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLObject)]
|
||||||
|
pub struct Album {
|
||||||
|
pub title: String,
|
||||||
|
pub release_date: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLObject)]
|
||||||
|
pub struct Artist {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash)]
|
||||||
|
pub enum QueueError {
|
||||||
|
ItemListNotQueuable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLObject)]
|
||||||
|
pub struct Queue {
|
||||||
|
pub tracks: Vec<Track>,
|
||||||
|
current: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queue {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tracks: vec![],
|
||||||
|
current: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.current = 0;
|
||||||
|
self.tracks = vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_with_track(&mut self, track: Track) {
|
||||||
|
self.current = 0;
|
||||||
|
self.tracks = vec![track];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_with_item_list(&mut self, item_list: &ItemList) -> Result<(), QueueError> {
|
||||||
|
if !item_list.is_queable {
|
||||||
|
return Err(QueueError::ItemListNotQueuable);
|
||||||
|
};
|
||||||
|
self.current = 0;
|
||||||
|
self.tracks = item_list.flatten();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current(&mut self, current: usize) -> bool {
|
||||||
|
if current < self.tracks.len() {
|
||||||
|
self.current = current as i32;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<&Track> {
|
||||||
|
self.tracks.get(self.current as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) -> Option<&Track> {
|
||||||
|
if (self.current as usize) < self.tracks.len() {
|
||||||
|
self.current += 1;
|
||||||
|
Some(&self.tracks[(self.current - 1) as usize])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) -> Option<&Track> {
|
||||||
|
if self.current > 0 {
|
||||||
|
self.current -= 1;
|
||||||
|
Some(&self.tracks[self.current as usize])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append_track(&mut self, track: Track) {
|
||||||
|
self.tracks.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append_playlist(&mut self, playlist: &[Track]) {
|
||||||
|
self.tracks.extend(playlist.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_track(&mut self, track: Track) {
|
||||||
|
self.tracks.insert(self.current as usize, track);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_playlist(&mut self, playlist: &[Track]) {
|
||||||
|
let tail: Vec<Track> = self
|
||||||
|
.tracks
|
||||||
|
.splice((self.current as usize).., playlist.to_vec())
|
||||||
|
.collect();
|
||||||
|
self.tracks.extend(tail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Mutation;
|
||||||
|
|
||||||
|
impl Mutation {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Mutation {
|
||||||
|
fn playpause(&self, track: InputTrack) -> FieldResult<ActiveTrack> {
|
||||||
|
Ok(ActiveTrack::new())
|
||||||
|
}
|
||||||
|
fn stop(&self, track: InputTrack) -> FieldResult<ActiveTrack> {
|
||||||
|
Ok(ActiveTrack::new())
|
||||||
|
}
|
||||||
|
fn previous(&self, track: InputTrack) -> FieldResult<ActiveTrack> {
|
||||||
|
Ok(ActiveTrack::new())
|
||||||
|
}
|
||||||
|
fn next(&self, track: InputTrack) -> FieldResult<ActiveTrack> {
|
||||||
|
Ok(ActiveTrack::new())
|
||||||
|
}
|
||||||
|
fn seek(&self, track: InputTrack, millis: i32) -> FieldResult<ActiveTrack> {
|
||||||
|
Ok(ActiveTrack::new())
|
||||||
|
}
|
||||||
|
fn append(&self, tracks: Vec<InputTrack>) -> FieldResult<Success> {
|
||||||
|
Ok(Success::Appending)
|
||||||
|
}
|
||||||
|
fn queue(&self, tracks: Vec<InputTrack>) -> FieldResult<Success> {
|
||||||
|
Ok(Success::Queuing)
|
||||||
|
}
|
||||||
|
fn replace(&self, tracks: Vec<InputTrack>) -> FieldResult<Success> {
|
||||||
|
Ok(Success::Replacing)
|
||||||
|
}
|
||||||
|
fn delete(&self, track: InputTrack) -> FieldResult<Success> {
|
||||||
|
Ok(Success::Deleting)
|
||||||
|
}
|
||||||
|
fn clear(&self, track: InputTrack) -> FieldResult<Success> {
|
||||||
|
Ok(Success::Clearing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLEnum)]
|
||||||
|
enum Success {
|
||||||
|
Appending,
|
||||||
|
Replacing,
|
||||||
|
Queuing,
|
||||||
|
Deleting,
|
||||||
|
Clearing,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, GraphQLObject)]
|
||||||
|
pub struct ActiveTrack {
|
||||||
|
track: Option<Track>,
|
||||||
|
completion: i32,
|
||||||
|
play_state: PlayState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveTrack {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
track: None,
|
||||||
|
completion: 0,
|
||||||
|
play_state: PlayState::Stopped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveTrackStream = Pin<Box<dyn Stream<Item = Result<ActiveTrack, FieldError>> + Send>>;
|
||||||
|
type QueueStream = Pin<Box<dyn Stream<Item = Result<Queue, FieldError>> + Send>>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Subscription {
|
||||||
|
queue: Queue,
|
||||||
|
active_track: Option<ActiveTrack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_subscription(context = Context)]
|
||||||
|
impl Subscription {
|
||||||
|
async fn queue() -> QueueStream {
|
||||||
|
let stream = futures::stream::iter(vec![Ok(Queue::new())]);
|
||||||
|
Box::pin(stream)
|
||||||
|
}
|
||||||
|
async fn active_track() -> ActiveTrackStream {
|
||||||
|
let stream = futures::stream::iter(vec![Ok(ActiveTrack::new())]);
|
||||||
|
Box::pin(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Subscription {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
queue: Queue::new(),
|
||||||
|
active_track: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
clients: HashMap<String, Arc<dyn ProviderClient>>,
|
||||||
|
queue: Queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(clients: HashMap<String, Arc<dyn ProviderClient>>) -> Self {
|
||||||
|
let queue = Queue::new();
|
||||||
|
Self { clients, queue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl juniper::Context for Context {}
|
||||||
|
|
||||||
|
pub type Schema = RootNode<'static, ItemList, Mutation, Subscription>;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
query ItemList {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#![allow(clippy::all, warnings)]
|
||||||
|
pub struct ItemList;
|
||||||
|
pub mod item_list {
|
||||||
|
#![allow(dead_code)]
|
||||||
|
use std::result::Result;
|
||||||
|
pub const OPERATION_NAME: &str = "ItemList";
|
||||||
|
pub const QUERY: &str = "query ItemList {\n name\n}\n";
|
||||||
|
use super::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type Boolean = bool;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type Float = f64;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type Int = i64;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
type ID = String;
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Variables;
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ResponseData {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl graphql_client::GraphQLQuery for ItemList {
|
||||||
|
type Variables = item_list::Variables;
|
||||||
|
type ResponseData = item_list::ResponseData;
|
||||||
|
fn build_query(variables: Self::Variables) -> ::graphql_client::QueryBody<Self::Variables> {
|
||||||
|
graphql_client::QueryBody {
|
||||||
|
variables,
|
||||||
|
query: item_list::QUERY,
|
||||||
|
operation_name: item_list::OPERATION_NAME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
type Queue {
|
||||||
|
tracks: [Track!]!
|
||||||
|
current: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
playpause(track: InputTrack!): ActiveTrack!
|
||||||
|
stop(track: InputTrack!): ActiveTrack!
|
||||||
|
previous(track: InputTrack!): ActiveTrack!
|
||||||
|
next(track: InputTrack!): ActiveTrack!
|
||||||
|
seek(track: InputTrack!, millis: Int!): ActiveTrack!
|
||||||
|
append(tracks: [InputTrack!]!): Success!
|
||||||
|
queue(tracks: [InputTrack!]!): Success!
|
||||||
|
replace(tracks: [InputTrack!]!): Success!
|
||||||
|
delete(track: InputTrack!): Success!
|
||||||
|
clear(track: InputTrack!): Success!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Album {
|
||||||
|
title: String!
|
||||||
|
releaseDate: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlayState {
|
||||||
|
BUFFERING
|
||||||
|
PLAYING
|
||||||
|
PAUSED
|
||||||
|
STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
queue: Queue!
|
||||||
|
activeTrack: ActiveTrack!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist {
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveTrack {
|
||||||
|
track: Track
|
||||||
|
completion: Int!
|
||||||
|
playState: PlayState!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemList {
|
||||||
|
name: String!
|
||||||
|
uuid: String!
|
||||||
|
provider: String!
|
||||||
|
tracks(refresh: Boolean!): [Track!]
|
||||||
|
children(refresh: Boolean!): [ItemList!]
|
||||||
|
isQueable: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track {
|
||||||
|
title: String!
|
||||||
|
uuid: String!
|
||||||
|
duration: Int
|
||||||
|
album: Album
|
||||||
|
artist: Artist
|
||||||
|
provider: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Success {
|
||||||
|
APPENDING
|
||||||
|
REPLACING
|
||||||
|
QUEUING
|
||||||
|
DELETING
|
||||||
|
CLEARING
|
||||||
|
}
|
||||||
|
|
||||||
|
input InputTrack {
|
||||||
|
uuid: String!
|
||||||
|
provider: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
schema {
|
||||||
|
query: ItemList
|
||||||
|
mutation: Mutation
|
||||||
|
subscription: Subscription
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue