A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 318 lines 7.6 kB view raw
1module Sources.Services.Ipfs exposing (..) 2 3{-| IPFS Service. 4 5Resources: 6 7 - <https://ipfs.io/docs/api/> 8 9-} 10 11import Common exposing (boolFromString, boolToString) 12import Conditional exposing (ifThenElse) 13import Dict 14import Dict.Ext as Dict 15import Http 16import Json.Decode as Json 17import Sources exposing (Property, SourceData) 18import Sources.Processing exposing (..) 19import Sources.Services.Ipfs.Marker as Marker 20import Sources.Services.Ipfs.Parser as Parser 21import String.Ext as String 22import Task 23import Time 24import Url 25 26 27 28-- PROPERTIES 29-- 📟 30 31 32defaults = 33 { gateway = "" 34 , local = boolToString False 35 , name = "Music from IPFS" 36 , ipns = boolToString False 37 } 38 39 40defaultGateway = 41 "https://ipfs.io" 42 43 44{-| The list of properties we need from the user. 45 46Tuple: (property, label, placeholder, isPassword) 47Will be used for the forms. 48 49-} 50properties : List Property 51properties = 52 [ { key = "directoryHash" 53 , label = "Directory hash / DNSLink domain" 54 , placeholder = "QmVLDAhCY3X9P2u" 55 , password = False 56 } 57 , { key = "ipns" 58 , label = "Resolve using IPNS" 59 , placeholder = defaults.ipns 60 , password = False 61 } 62 , { key = "gateway" 63 , label = "Gateway (Optional)" 64 , placeholder = defaultGateway 65 , password = False 66 } 67 , { key = "local" 68 , label = "Resolve IPNS locally" 69 , placeholder = defaults.local 70 , password = False 71 } 72 ] 73 74 75{-| Initial data set. 76-} 77initialData : SourceData 78initialData = 79 Dict.fromList 80 [ ( "directoryHash", "" ) 81 , ( "gateway", defaults.gateway ) 82 , ( "ipns", defaults.ipns ) 83 , ( "local", defaults.local ) 84 , ( "name", defaults.name ) 85 ] 86 87 88 89-- PREPARATION 90 91 92prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) 93prepare _ srcData _ toMsg = 94 let 95 domainName = 96 srcData 97 |> Dict.get "directoryHash" 98 |> Maybe.withDefault "" 99 |> String.chopStart "http://" 100 |> String.chopStart "https://" 101 |> String.chopEnd "/" 102 |> String.chopStart "_dnslink." 103 in 104 if isDnsLink srcData then 105 (Just << Http.request) 106 { method = "POST" 107 , headers = [] 108 , url = extractGateway srcData ++ "/api/v0/dns?arg=" ++ domainName 109 , body = Http.emptyBody 110 , expect = Http.expectString toMsg 111 , timeout = Nothing 112 , tracker = Nothing 113 } 114 115 else 116 Nothing 117 118 119 120-- TREE 121 122 123{-| Create a directory tree. 124-} 125makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg 126makeTree srcData marker _ resultMsg = 127 let 128 gateway = 129 extractGateway srcData 130 131 resolveWithIpns = 132 case marker of 133 InProgress _ -> 134 False 135 136 _ -> 137 srcData 138 |> Dict.fetch "ipns" defaults.ipns 139 |> boolFromString 140 141 resolveLocally = 142 srcData 143 |> Dict.fetch "local" defaults.local 144 |> boolFromString 145 |> (\b -> ifThenElse b "true" "false") 146 147 root = 148 rootHash srcData 149 150 path = 151 case marker of 152 InProgress _ -> 153 marker 154 |> Marker.takeOne 155 |> Maybe.map (\p -> root ++ "/" ++ p) 156 |> Maybe.withDefault "" 157 158 _ -> 159 root 160 in 161 (if resolveWithIpns then 162 Http.task 163 { method = "POST" 164 , headers = [] 165 , url = gateway ++ "/api/v0/name/resolve?arg=" ++ encodedPath path ++ "&local=" ++ resolveLocally ++ "&encoding=json" 166 , body = Http.emptyBody 167 , resolver = Http.stringResolver ipnsResolver 168 , timeout = Just (60 * 15 * 1000) 169 } 170 171 else 172 Task.succeed { ipfsPath = path } 173 ) 174 |> Task.andThen 175 (\{ ipfsPath } -> 176 Http.task 177 { method = "POST" 178 , headers = [] 179 , url = gateway ++ "/api/v0/ls?arg=" ++ encodedPath ipfsPath ++ "&encoding=json" 180 , body = Http.emptyBody 181 , resolver = Http.stringResolver Common.translateHttpResponse 182 , timeout = Just (60 * 15 * 1000) 183 } 184 ) 185 |> Task.attempt resultMsg 186 187 188ipnsResolver : Http.Response String -> Result Http.Error { ipfsPath : String } 189ipnsResolver response = 190 case response of 191 Http.BadUrl_ u -> 192 Err (Http.BadUrl u) 193 194 Http.Timeout_ -> 195 Err Http.Timeout 196 197 Http.NetworkError_ -> 198 Err Http.NetworkError 199 200 Http.BadStatus_ _ body -> 201 Err (Http.BadBody body) 202 203 Http.GoodStatus_ _ body -> 204 body 205 |> Json.decodeString (Json.field "Path" Json.string) 206 |> Result.map (\path -> { ipfsPath = String.chopStart "/ipfs/" path }) 207 |> Result.mapError (Json.errorToString >> Http.BadBody) 208 209 210{-| Re-export parser functions. 211-} 212parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 213parsePreparationResponse = 214 Parser.parseDnsLookup 215 216 217parseTreeResponse : String -> Marker -> TreeAnswer Marker 218parseTreeResponse = 219 Parser.parseTreeResponse 220 221 222parseErrorResponse : String -> Maybe String 223parseErrorResponse = 224 Parser.parseErrorResponse 225 226 227 228-- POST 229 230 231{-| Post process the tree results. 232 233!!! Make sure we only use music files that we can use. 234 235-} 236postProcessTree : List String -> List String 237postProcessTree = 238 identity 239 240 241 242-- TRACK URL 243 244 245{-| Create a public url for a file. 246 247We need this to play the track. 248 249-} 250makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 251makeTrackUrl _ _ srcData _ path = 252 if not (String.contains "/" path) && not (String.contains "." path) then 253 -- If it still uses the old way of doing things 254 -- (ie. each path was a cid) 255 extractGateway srcData ++ "/ipfs/" ++ path 256 257 else 258 -- Or the new way 259 extractGateway srcData ++ "/ipfs/" ++ rootHash srcData ++ "/" ++ encodedPath path 260 261 262 263-- ⚗️ 264 265 266encodedPath : String -> String 267encodedPath path = 268 path 269 |> String.split "/" 270 |> List.map Url.percentEncode 271 |> String.join "/" 272 273 274extractGateway : SourceData -> String 275extractGateway srcData = 276 srcData 277 |> Dict.get "gateway" 278 |> Maybe.map String.trim 279 |> Maybe.andThen 280 (\s -> 281 case s of 282 "" -> 283 Nothing 284 285 _ -> 286 Just s 287 ) 288 |> Maybe.map (String.chopEnd "/") 289 |> Maybe.withDefault defaultGateway 290 291 292isDnsLink : SourceData -> Bool 293isDnsLink srcData = 294 srcData 295 |> Dict.get "directoryHash" 296 |> Maybe.map pathIsDnsLink 297 |> Maybe.withDefault False 298 299 300pathIsDnsLink : String -> Bool 301pathIsDnsLink = 302 String.contains "." 303 304 305rootHash : SourceData -> String 306rootHash srcData = 307 srcData 308 |> Dict.get "directoryHash" 309 |> Maybe.andThen 310 (\path -> 311 if pathIsDnsLink path then 312 Dict.get "directoryHashFromDnsLink" srcData 313 314 else 315 Just path 316 ) 317 |> Maybe.withDefault "" 318 |> String.chopEnd "/"