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