···11-module Dict.Ext exposing (fetch, fetchUnknown)
11+module Dict.Ext exposing (fetch, fetchUnknown, unionFlipped)
2233import Dict exposing (Dict)
44+55+66+unionFlipped : Dict comparable v -> Dict comparable v -> Dict comparable v
77+unionFlipped a b =
88+ Dict.union b a
49510611fetch : comparable -> v -> Dict comparable v -> v
···77import Sources exposing (..)
88import Sources.Processing exposing (..)
99import Sources.Services.AmazonS3 as AmazonS3
1010+import Sources.Services.Dropbox as Dropbox
1111+import Sources.Services.Google as Google
1212+import Sources.Services.Ipfs as Ipfs
1013import Time
11141215···2023 AmazonS3 ->
2124 AmazonS3.initialData
22252626+ Dropbox ->
2727+ Dropbox.initialData
2828+2929+ Ipfs ->
3030+ Ipfs.initialData
3131+3232+ Google ->
3333+ Google.initialData
3434+23352436makeTrackUrl : Service -> Time.Posix -> SourceData -> HttpMethod -> String -> String
2537makeTrackUrl service =
···2739 AmazonS3 ->
2840 AmazonS3.makeTrackUrl
29414242+ Dropbox ->
4343+ Dropbox.makeTrackUrl
4444+4545+ Ipfs ->
4646+ Ipfs.makeTrackUrl
4747+4848+ Google ->
4949+ Google.makeTrackUrl
5050+30513152makeTree :
3253 Service
···4061 AmazonS3 ->
4162 AmazonS3.makeTree
42636464+ Dropbox ->
6565+ Dropbox.makeTree
6666+6767+ Ipfs ->
6868+ Ipfs.makeTree
6969+7070+ Google ->
7171+ Google.makeTree
7272+43734474parseErrorResponse : Service -> String -> String
4575parseErrorResponse service =
···4777 AmazonS3 ->
4878 AmazonS3.parseErrorResponse
49798080+ Dropbox ->
8181+ Dropbox.parseErrorResponse
8282+8383+ Ipfs ->
8484+ Ipfs.parseErrorResponse
8585+8686+ Google ->
8787+ Google.parseErrorResponse
8888+50895190parsePreparationResponse : Service -> String -> SourceData -> Marker -> PrepationAnswer Marker
5291parsePreparationResponse service =
···5493 AmazonS3 ->
5594 AmazonS3.parsePreparationResponse
56959696+ Dropbox ->
9797+ Dropbox.parsePreparationResponse
9898+9999+ Ipfs ->
100100+ Ipfs.parsePreparationResponse
101101+102102+ Google ->
103103+ Google.parsePreparationResponse
104104+5710558106parseTreeResponse : Service -> String -> Marker -> TreeAnswer Marker
59107parseTreeResponse service =
···61109 AmazonS3 ->
62110 AmazonS3.parseTreeResponse
63111112112+ Dropbox ->
113113+ Dropbox.parseTreeResponse
114114+115115+ Ipfs ->
116116+ Ipfs.parseTreeResponse
117117+118118+ Google ->
119119+ Google.parseTreeResponse
120120+6412165122postProcessTree : Service -> List String -> List String
66123postProcessTree service =
67124 case service of
68125 AmazonS3 ->
69126 AmazonS3.postProcessTree
127127+128128+ Dropbox ->
129129+ Dropbox.postProcessTree
130130+131131+ Ipfs ->
132132+ Ipfs.postProcessTree
133133+134134+ Google ->
135135+ Google.postProcessTree
701367113772138prepare :
···81147 AmazonS3 ->
82148 AmazonS3.prepare
83149150150+ Dropbox ->
151151+ Dropbox.prepare
152152+153153+ Ipfs ->
154154+ Ipfs.prepare
155155+156156+ Google ->
157157+ Google.prepare
158158+8415985160properties : Service -> List Property
86161properties service =
···88163 AmazonS3 ->
89164 AmazonS3.properties
90165166166+ Dropbox ->
167167+ Dropbox.properties
168168+169169+ Ipfs ->
170170+ Ipfs.properties
171171+172172+ Google ->
173173+ Google.properties
174174+911759217693177-- KEYS & LABELS
···99183 "AmazonS3" ->
100184 Just AmazonS3
101185186186+ "Dropbox" ->
187187+ Just Dropbox
188188+189189+ "Ipfs" ->
190190+ Just Ipfs
191191+192192+ "Google" ->
193193+ Just Google
194194+102195 _ ->
103196 Nothing
104197···109202 AmazonS3 ->
110203 "AmazonS3"
111204205205+ Dropbox ->
206206+ "Dropbox"
207207+208208+ Ipfs ->
209209+ "Ipfs"
210210+211211+ Google ->
212212+ "Google"
213213+112214113215{-| Service labels.
114216Maps a service key to a label.
···116218labels : List ( String, String )
117219labels =
118220 [ ( typeToKey AmazonS3, "Amazon S3" )
221221+ , ( typeToKey Dropbox, "Dropbox" )
222222+ , ( typeToKey Google, "Google Drive" )
223223+ , ( typeToKey Ipfs, "IPFS" )
119224 ]
+3-3
src/Library/Sources/Services/AmazonS3.elm
···10101111import Dict
1212import Http
1313-import Sources exposing (..)
1313+import Sources exposing (Property, SourceData)
1414import Sources.Pick
1515import Sources.Processing exposing (..)
1616import Sources.Services.AmazonS3.Parser as Parser
···164164165165166166167167--- Post
167167+-- POST
168168169169170170{-| Post process the tree results.
···178178179179180180181181--- Track URL
181181+-- TRACK URL
182182183183184184{-| Create a public url for a file.
+240
src/Library/Sources/Services/Dropbox.elm
···11+module Sources.Services.Dropbox exposing (authorizationSourceData, authorizationUrl, defaults, getProperDirectoryPath, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties)
22+33+{-| Dropbox Service.
44+-}
55+66+import Base64
77+import Dict
88+import Dict.Ext as Dict
99+import Http
1010+import Json.Decode
1111+import Json.Encode
1212+import Regex
1313+import Sources exposing (Property, SourceData)
1414+import Sources.Pick
1515+import Sources.Processing exposing (..)
1616+import Sources.Services.Common exposing (cleanPath, noPrep)
1717+import Sources.Services.Dropbox.Parser as Parser
1818+import Time
1919+import Url
2020+import Url.Builder as Url
2121+2222+2323+2424+-- PROPERTIES
2525+-- 📟
2626+2727+2828+defaults =
2929+ { appKey = "kwsydtrzban41zr"
3030+ , directoryPath = "/"
3131+ , name = "Music from Dropbox"
3232+ }
3333+3434+3535+{-| The list of properties we need from the user.
3636+3737+Tuple: (property, label, placeholder, isPassword)
3838+Will be used for the forms.
3939+4040+-}
4141+properties : List Property
4242+properties =
4343+ [ { key = "accessToken"
4444+ , label = "Access Token"
4545+ , placeholder = "..."
4646+ , password = True
4747+ }
4848+ , { key = "appKey"
4949+ , label = "App key"
5050+ , placeholder = defaults.appKey
5151+ , password = False
5252+ }
5353+ , { key = "directoryPath"
5454+ , label = "Directory"
5555+ , placeholder = defaults.directoryPath
5656+ , password = False
5757+ }
5858+ ]
5959+6060+6161+{-| Initial data set.
6262+-}
6363+initialData : SourceData
6464+initialData =
6565+ Dict.fromList
6666+ [ ( "accessToken", "" )
6767+ , ( "appKey", defaults.appKey )
6868+ , ( "directoryPath", defaults.directoryPath )
6969+ , ( "name", defaults.name )
7070+ ]
7171+7272+7373+7474+-- AUTHORIZATION
7575+7676+7777+{-| Authorization url.
7878+-}
7979+authorizationUrl : SourceData -> String -> String
8080+authorizationUrl sourceData origin =
8181+ let
8282+ encodeData data =
8383+ data
8484+ |> Dict.toList
8585+ |> List.map (Tuple.mapSecond Json.Encode.string)
8686+ |> Json.Encode.object
8787+8888+ state =
8989+ sourceData
9090+ |> encodeData
9191+ |> Json.Encode.encode 0
9292+ |> Base64.encode
9393+ in
9494+ [ ( "response_type", "token" )
9595+ , ( "client_id", Dict.fetch "appKey" "unknown" sourceData )
9696+ , ( "redirect_uri", origin ++ "/sources/new/dropbox" )
9797+ , ( "state", state )
9898+ ]
9999+ |> List.map (\( a, b ) -> Url.string a b)
100100+ |> Url.toQuery
101101+ |> String.append "https://www.dropbox.com/oauth2/authorize"
102102+103103+104104+{-| Authorization source data.
105105+-}
106106+authorizationSourceData : { codeOrToken : Maybe String, state : Maybe String } -> SourceData
107107+authorizationSourceData args =
108108+ args.state
109109+ |> Maybe.andThen (Base64.decode >> Result.toMaybe)
110110+ |> Maybe.withDefault "{}"
111111+ |> Json.Decode.decodeString (Json.Decode.dict Json.Decode.string)
112112+ |> Result.withDefault Dict.empty
113113+ |> Dict.unionFlipped initialData
114114+ |> Dict.update "accessToken" (\_ -> args.codeOrToken)
115115+116116+117117+118118+-- PREPARATION
119119+120120+121121+prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg)
122122+prepare _ _ _ _ =
123123+ Nothing
124124+125125+126126+127127+-- TREE
128128+129129+130130+{-| Create a directory tree.
131131+132132+List all the tracks in the bucket.
133133+Or a specific directory in the bucket.
134134+135135+-}
136136+makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg
137137+makeTree srcData marker currentTime toMsg =
138138+ let
139139+ accessToken =
140140+ Dict.fetch "accessToken" "" srcData
141141+142142+ body =
143143+ (case marker of
144144+ TheBeginning ->
145145+ [ ( "limit", Json.Encode.int 2000 )
146146+ , ( "path", Json.Encode.string (getProperDirectoryPath srcData) )
147147+ , ( "recursive", Json.Encode.bool True )
148148+ ]
149149+150150+ InProgress cursor ->
151151+ [ ( "cursor", Json.Encode.string cursor )
152152+ ]
153153+154154+ TheEnd ->
155155+ []
156156+ )
157157+ |> Json.Encode.object
158158+ |> Http.jsonBody
159159+160160+ url =
161161+ case marker of
162162+ TheBeginning ->
163163+ "https://api.dropboxapi.com/2/files/list_folder"
164164+165165+ InProgress _ ->
166166+ "https://api.dropboxapi.com/2/files/list_folder/continue"
167167+168168+ TheEnd ->
169169+ ""
170170+ in
171171+ Http.request
172172+ { method = "POST"
173173+ , headers = [ Http.header "Authorization" ("Bearer " ++ accessToken) ]
174174+ , url = url
175175+ , body = body
176176+ , expect = Http.expectString toMsg
177177+ , timeout = Nothing
178178+ , tracker = Nothing
179179+ }
180180+181181+182182+getProperDirectoryPath : SourceData -> String
183183+getProperDirectoryPath srcData =
184184+ let
185185+ path =
186186+ srcData
187187+ |> Dict.get "directoryPath"
188188+ |> Maybe.withDefault defaults.directoryPath
189189+ |> cleanPath
190190+ in
191191+ if path == "" then
192192+ ""
193193+194194+ else
195195+ "/" ++ path
196196+197197+198198+{-| Re-export parser functions.
199199+-}
200200+parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker
201201+parsePreparationResponse =
202202+ noPrep
203203+204204+205205+parseTreeResponse : String -> Marker -> TreeAnswer Marker
206206+parseTreeResponse =
207207+ Parser.parseTreeResponse
208208+209209+210210+parseErrorResponse : String -> String
211211+parseErrorResponse =
212212+ Parser.parseErrorResponse
213213+214214+215215+216216+-- POST
217217+218218+219219+{-| Post process the tree results.
220220+221221+!!! Make sure we only use music files that we can use.
222222+223223+-}
224224+postProcessTree : List String -> List String
225225+postProcessTree =
226226+ Sources.Pick.selectMusicFiles
227227+228228+229229+230230+-- TRACK URL
231231+232232+233233+{-| Create a public url for a file.
234234+235235+We need this to play the track.
236236+237237+-}
238238+makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String
239239+makeTrackUrl currentTime srcData method pathToFile =
240240+ "dropbox://" ++ Dict.fetch "accessToken" "" srcData ++ "@" ++ pathToFile
···11+module Sources.Services.Ipfs exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties)
22+33+{-| IPFS Service.
44+55+Resources:
66+77+ - <https://ipfs.io/docs/api/>
88+99+-}
1010+1111+import Dict
1212+import Http
1313+import Sources exposing (Property, SourceData)
1414+import Sources.Processing exposing (..)
1515+import Sources.Services.Common exposing (cleanPath, noPrep)
1616+import Sources.Services.Ipfs.Marker as Marker
1717+import Sources.Services.Ipfs.Parser as Parser
1818+import Time
1919+2020+2121+2222+-- PROPERTIES
2323+-- 📟
2424+2525+2626+defaults =
2727+ { gateway = "http://localhost:8080"
2828+ , name = "Music from IPFS"
2929+ }
3030+3131+3232+{-| The list of properties we need from the user.
3333+3434+Tuple: (property, label, placeholder, isPassword)
3535+Will be used for the forms.
3636+3737+-}
3838+properties : List Property
3939+properties =
4040+ [ { key = "directoryHash"
4141+ , label = "Directory object hash"
4242+ , placeholder = "QmVLDAhCY3X9P2u"
4343+ , password = False
4444+ }
4545+ , { key = "gateway"
4646+ , label = "Read-only gateway"
4747+ , placeholder = defaults.gateway
4848+ , password = False
4949+ }
5050+ ]
5151+5252+5353+{-| Initial data set.
5454+-}
5555+initialData : SourceData
5656+initialData =
5757+ Dict.fromList
5858+ [ ( "directoryHash", "" )
5959+ , ( "name", defaults.name )
6060+ , ( "gateway", defaults.gateway )
6161+ ]
6262+6363+6464+6565+-- PREPARATION
6666+6767+6868+prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg)
6969+prepare _ _ _ _ =
7070+ Nothing
7171+7272+7373+7474+-- TREE
7575+7676+7777+{-| Create a directory tree.
7878+-}
7979+makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg
8080+makeTree srcData marker _ resultMsg =
8181+ let
8282+ gateway =
8383+ srcData
8484+ |> Dict.get "gateway"
8585+ |> Maybe.withDefault defaults.gateway
8686+ |> String.foldr
8787+ (\char acc ->
8888+ if String.isEmpty acc && char == '/' then
8989+ acc
9090+9191+ else
9292+ String.cons char acc
9393+ )
9494+ ""
9595+9696+ hash =
9797+ case marker of
9898+ InProgress _ ->
9999+ marker
100100+ |> Marker.takeOne
101101+ |> Maybe.withDefault "MISSING_HASH"
102102+103103+ _ ->
104104+ srcData
105105+ |> Dict.get "directoryHash"
106106+ |> Maybe.withDefault "MISSING_HASH"
107107+108108+ url =
109109+ gateway ++ "/api/v0/ls?arg=" ++ hash ++ "&encoding=json"
110110+ in
111111+ Http.get
112112+ { url = url
113113+ , expect = Http.expectString resultMsg
114114+ }
115115+116116+117117+{-| Re-export parser functions.
118118+-}
119119+parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker
120120+parsePreparationResponse =
121121+ noPrep
122122+123123+124124+parseTreeResponse : String -> Marker -> TreeAnswer Marker
125125+parseTreeResponse =
126126+ Parser.parseTreeResponse
127127+128128+129129+parseErrorResponse : String -> String
130130+parseErrorResponse =
131131+ identity
132132+133133+134134+135135+-- POST
136136+137137+138138+{-| Post process the tree results.
139139+140140+!!! Make sure we only use music files that we can use.
141141+142142+-}
143143+postProcessTree : List String -> List String
144144+postProcessTree =
145145+ identity
146146+147147+148148+149149+-- TRACK URL
150150+151151+152152+{-| Create a public url for a file.
153153+154154+We need this to play the track.
155155+156156+-}
157157+makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String
158158+makeTrackUrl _ srcData _ hash =
159159+ let
160160+ gateway =
161161+ srcData
162162+ |> Dict.get "gateway"
163163+ |> Maybe.withDefault defaults.gateway
164164+ |> String.foldr
165165+ (\char acc ->
166166+ if String.isEmpty acc && char == '/' then
167167+ acc
168168+169169+ else
170170+ String.cons char acc
171171+ )
172172+ ""
173173+ in
174174+ gateway ++ "/ipfs/" ++ hash
+93
src/Library/Sources/Services/Ipfs/Marker.elm
···11+module Sources.Services.Ipfs.Marker exposing (concat, removeOne, separator, takeOne)
22+33+{-| Marker stuff for IPFS.
44+55+The IPFS API currently doesn't have a way to return a tree.
66+So we have build one ourselves.
77+88+How it works:
99+1010+1. We list the objects in a given directory
1111+2. We make a list of the sub directories
1212+3. The marker becomes either:
1313+ - InProgress `hashOfSubDirA/hashOfSubDirB/hashOfSubDirC`
1414+ - TheEnd
1515+4. If the marker was of the type `InProgress`,
1616+ the next request will make a list of the objects in `hashOfSubDirA`.
1717+ And so on ...
1818+1919+-}
2020+2121+import Sources.Processing exposing (Marker(..))
2222+2323+2424+separator : String
2525+separator =
2626+ " ɑ "
2727+2828+2929+3030+-- In
3131+3232+3333+concat : List String -> Marker -> Marker
3434+concat list marker =
3535+ let
3636+ result =
3737+ case marker of
3838+ InProgress m ->
3939+ [ list, String.split separator m ]
4040+ |> List.concat
4141+ |> String.join separator
4242+4343+ _ ->
4444+ String.join separator list
4545+ in
4646+ case result of
4747+ "" ->
4848+ TheEnd
4949+5050+ r ->
5151+ InProgress r
5252+5353+5454+5555+-- Out
5656+5757+5858+{-| Take the first item and return it.
5959+-}
6060+takeOne : Marker -> Maybe String
6161+takeOne marker =
6262+ case marker of
6363+ InProgress m ->
6464+ m
6565+ |> String.split separator
6666+ |> List.head
6767+6868+ _ ->
6969+ Nothing
7070+7171+7272+{-| Remove the first item if there is one.
7373+-}
7474+removeOne : Marker -> Marker
7575+removeOne marker =
7676+ case marker of
7777+ InProgress m ->
7878+ let
7979+ tmp =
8080+ m
8181+ |> String.split separator
8282+ |> List.drop 1
8383+ |> String.join separator
8484+ in
8585+ case tmp of
8686+ "" ->
8787+ TheEnd
8888+8989+ x ->
9090+ InProgress x
9191+9292+ _ ->
9393+ TheEnd
+65
src/Library/Sources/Services/Ipfs/Parser.elm
···11+module Sources.Services.Ipfs.Parser exposing (Link, linkDecoder, parseTreeResponse, treeDecoder)
22+33+import Json.Decode exposing (..)
44+import Sources.Pick exposing (isMusicFile)
55+import Sources.Processing exposing (Marker(..), TreeAnswer)
66+import Sources.Services.Ipfs.Marker as Marker
77+88+99+1010+-- TREE
1111+1212+1313+parseTreeResponse : String -> Marker -> TreeAnswer Marker
1414+parseTreeResponse response previousMarker =
1515+ let
1616+ links =
1717+ case decodeString treeDecoder response of
1818+ Ok l ->
1919+ l
2020+2121+ Err _ ->
2222+ []
2323+2424+ dirs =
2525+ links
2626+ |> List.filter (.typ >> (==) 1)
2727+ |> List.map .hash
2828+2929+ files =
3030+ links
3131+ |> List.filter (.typ >> (==) 2)
3232+ |> List.filter (.name >> isMusicFile)
3333+ |> List.map .hash
3434+ in
3535+ { filePaths =
3636+ files
3737+ , marker =
3838+ previousMarker
3939+ |> Marker.removeOne
4040+ |> Marker.concat dirs
4141+ }
4242+4343+4444+treeDecoder : Decoder (List Link)
4545+treeDecoder =
4646+ field "Objects" <| index 0 <| field "Links" <| list linkDecoder
4747+4848+4949+5050+-- Links
5151+5252+5353+type alias Link =
5454+ { hash : String
5555+ , name : String
5656+ , typ : Int
5757+ }
5858+5959+6060+linkDecoder : Decoder Link
6161+linkDecoder =
6262+ map3 Link
6363+ (field "Hash" string)
6464+ (field "Name" string)
6565+ (field "Type" int)