Add initial draft for core

First attempt for common types and the graphql schema
This commit is contained in:
Hans Mündelein 2023-05-17 18:30:26 +02:00
commit 91e988f722
Signed by: hans
GPG Key ID: BA7B55E984CE74F4
10 changed files with 1892 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2
Cargo.toml Normal file
View File

@ -0,0 +1,2 @@
[workspace]
members = ["crabidy-core"]

BIN
Draft.rnote Normal file

Binary file not shown.

24
crabidy-core/Cargo.toml Normal file
View File

@ -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"] }

32
crabidy-core/build.rs Normal file
View File

@ -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();
}

328
crabidy-core/src/lib.rs Normal file
View File

@ -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>;

View File

@ -0,0 +1,3 @@
query ItemList {
name
}

35
crabidy-core/src/query.rs Normal file
View File

@ -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,
}
}
}

View File

@ -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
}

1386
crabidy-core/src/schema.json Normal file

File diff suppressed because it is too large Load Diff