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.

Add AmazonS3 service

+721 -6
+9 -4
elm.json
··· 15 15 "elm/html": "1.0.0", 16 16 "elm/http": "2.0.0", 17 17 "elm/json": "1.1.2", 18 + "elm/regex": "1.0.0", 18 19 "elm/svg": "1.0.1", 19 20 "elm/time": "1.0.0", 20 21 "elm/url": "1.0.0", 21 22 "elm-community/list-extra": "8.1.0", 23 + "elm-community/maybe-extra": "5.0.0", 22 24 "icidasset/elm-binary": "1.3.0", 23 25 "icidasset/elm-sha": "1.0.0", 24 26 "jorgengranseth/elm-string-format": "1.0.1", 25 27 "justgage/tachyons-elm": "4.1.1", 26 - "rtfeldman/elm-hex": "1.0.0" 28 + "ryannhg/date-format": "2.3.0", 29 + "ymtszw/elm-xml-decode": "3.0.0" 27 30 }, 28 31 "indirect": { 29 32 "elm/bytes": "1.0.7", 30 33 "elm/file": "1.0.1", 31 - "elm/regex": "1.0.0", 32 - "elm/virtual-dom": "1.0.2" 34 + "elm/parser": "1.1.0", 35 + "elm/virtual-dom": "1.0.2", 36 + "jinjor/elm-xml-parser": "2.0.0", 37 + "rtfeldman/elm-hex": "1.0.0" 33 38 } 34 39 }, 35 40 "test-dependencies": { ··· 38 43 "elm/random": "1.0.0" 39 44 } 40 45 } 41 - } 46 + }
+1 -1
src/Library/Crypto/Hmac.elm
··· 1 - module Crypto.Hmac exposing (encrypt128, encrypt64) 1 + module Crypto.HMAC exposing (encrypt128, encrypt64) 2 2 3 3 {-| Cryptography – HMAC 4 4 -}
+15
src/Library/Dict/Ext.elm
··· 1 + module Dict.Ext exposing (fetch, fetchUnknown) 2 + 3 + import Dict exposing (Dict) 4 + 5 + 6 + fetch : comparable -> v -> Dict comparable v -> v 7 + fetch key default dict = 8 + dict 9 + |> Dict.get key 10 + |> Maybe.withDefault default 11 + 12 + 13 + fetchUnknown : comparable -> Dict comparable String -> String 14 + fetchUnknown key dict = 15 + fetch key "MISSING_VALUE" dict
+13
src/Library/Maybe/Ext.elm
··· 1 + module Maybe.Ext exposing (preferFirst, preferSecond) 2 + 3 + import Maybe.Extra 4 + 5 + 6 + preferFirst : Maybe a -> Maybe a -> Maybe a 7 + preferFirst = 8 + Maybe.Extra.or 9 + 10 + 11 + preferSecond : Maybe a -> Maybe a -> Maybe a 12 + preferSecond = 13 + Maybe.Extra.orElse
+13 -1
src/Library/Sources.elm
··· 1 - module Sources exposing (Form, FormStep(..), Page(..), Service(..), Source, SourceData, defaultService, emptySource, newForm) 1 + module Sources exposing (Form, FormStep(..), Page(..), Property, Service(..), Source, SourceData, defaultService, emptySource, newForm) 2 2 3 3 import Dict exposing (Dict) 4 4 ··· 20 20 -- PIECES 21 21 22 22 23 + type alias Property = 24 + { prop : String 25 + , labl : String 26 + , plho : String 27 + , pass : Bool 28 + } 29 + 30 + 23 31 type alias SourceData = 24 32 Dict String String 33 + 34 + 35 + 36 + -- SERVICES 25 37 26 38 27 39 type Service
+24
src/Library/Sources/Pick.elm
··· 1 + module Sources.Pick exposing (isMusicFile, selectMusicFiles) 2 + 3 + import Regex 4 + 5 + 6 + isMusicFile : String -> Bool 7 + isMusicFile = 8 + Regex.contains musicFileRegex 9 + 10 + 11 + selectMusicFiles : List String -> List String 12 + selectMusicFiles = 13 + List.filter isMusicFile 14 + 15 + 16 + 17 + -- PRIVATE 18 + 19 + 20 + musicFileRegex : Regex.Regex 21 + musicFileRegex = 22 + "\\.(mp3|mp4|m4a|flac)$" 23 + |> Regex.fromStringWith { caseInsensitive = True, multiline = False } 24 + |> Maybe.withDefault Regex.never
+45
src/Library/Sources/Processing.elm
··· 1 + module Sources.Processing exposing (HttpMethod(..), Marker(..), PrepationAnswer, TreeAnswer, httpMethod) 2 + 3 + import Http 4 + import Sources exposing (SourceData) 5 + 6 + 7 + 8 + -- MARKERS & RESPONSES 9 + 10 + 11 + type Marker 12 + = TheBeginning 13 + | InProgress String 14 + | TheEnd 15 + 16 + 17 + type alias PrepationAnswer marker = 18 + { sourceData : SourceData 19 + , marker : marker 20 + } 21 + 22 + 23 + type alias TreeAnswer marker = 24 + { filePaths : List String 25 + , marker : marker 26 + } 27 + 28 + 29 + 30 + -- HTTP 31 + 32 + 33 + type HttpMethod 34 + = Get 35 + | Head 36 + 37 + 38 + httpMethod : HttpMethod -> String 39 + httpMethod method = 40 + case method of 41 + Get -> 42 + "GET" 43 + 44 + Head -> 45 + "HEAD"
+119
src/Library/Sources/Services.elm
··· 1 + module Sources.Services exposing (initialData, keyToType, labels, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties, typeToKey) 2 + 3 + {-| Service functions used in other modules. 4 + -} 5 + 6 + import Http 7 + import Sources exposing (..) 8 + import Sources.Processing exposing (..) 9 + import Sources.Services.AmazonS3 as AmazonS3 10 + import Time 11 + 12 + 13 + 14 + -- FUNCTIONS 15 + 16 + 17 + initialData : Service -> SourceData 18 + initialData service = 19 + case service of 20 + AmazonS3 -> 21 + AmazonS3.initialData 22 + 23 + 24 + makeTrackUrl : Service -> Time.Posix -> SourceData -> HttpMethod -> String -> String 25 + makeTrackUrl service = 26 + case service of 27 + AmazonS3 -> 28 + AmazonS3.makeTrackUrl 29 + 30 + 31 + makeTree : 32 + Service 33 + -> SourceData 34 + -> Marker 35 + -> Time.Posix 36 + -> (Result Http.Error String -> msg) 37 + -> Cmd msg 38 + makeTree service = 39 + case service of 40 + AmazonS3 -> 41 + AmazonS3.makeTree 42 + 43 + 44 + parseErrorResponse : Service -> String -> String 45 + parseErrorResponse service = 46 + case service of 47 + AmazonS3 -> 48 + AmazonS3.parseErrorResponse 49 + 50 + 51 + parsePreparationResponse : Service -> String -> SourceData -> Marker -> PrepationAnswer Marker 52 + parsePreparationResponse service = 53 + case service of 54 + AmazonS3 -> 55 + AmazonS3.parsePreparationResponse 56 + 57 + 58 + parseTreeResponse : Service -> String -> Marker -> TreeAnswer Marker 59 + parseTreeResponse service = 60 + case service of 61 + AmazonS3 -> 62 + AmazonS3.parseTreeResponse 63 + 64 + 65 + postProcessTree : Service -> List String -> List String 66 + postProcessTree service = 67 + case service of 68 + AmazonS3 -> 69 + AmazonS3.postProcessTree 70 + 71 + 72 + prepare : 73 + Service 74 + -> String 75 + -> SourceData 76 + -> Marker 77 + -> (Result Http.Error String -> msg) 78 + -> Maybe (Cmd msg) 79 + prepare service = 80 + case service of 81 + AmazonS3 -> 82 + AmazonS3.prepare 83 + 84 + 85 + properties : Service -> List Property 86 + properties service = 87 + case service of 88 + AmazonS3 -> 89 + AmazonS3.properties 90 + 91 + 92 + 93 + -- KEYS & LABELS 94 + 95 + 96 + keyToType : String -> Maybe Service 97 + keyToType str = 98 + case str of 99 + "AmazonS3" -> 100 + Just AmazonS3 101 + 102 + _ -> 103 + Nothing 104 + 105 + 106 + typeToKey : Service -> String 107 + typeToKey service = 108 + case service of 109 + AmazonS3 -> 110 + "AmazonS3" 111 + 112 + 113 + {-| Service labels. 114 + Maps a service key to a label. 115 + -} 116 + labels : List ( String, String ) 117 + labels = 118 + [ ( typeToKey AmazonS3, "Amazon S3" ) 119 + ]
+168
src/Library/Sources/Services/AmazonS3.elm
··· 1 + module Sources.Services.AmazonS3 exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) 2 + 3 + {-| Amazon S3 Service. 4 + 5 + Resources: 6 + 7 + - <http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html> 8 + 9 + -} 10 + 11 + import Dict 12 + import Http 13 + import Sources exposing (..) 14 + import Sources.Pick 15 + import Sources.Processing exposing (..) 16 + import Sources.Services.AmazonS3.Parser as Parser 17 + import Sources.Services.AmazonS3.Presign exposing (..) 18 + import Sources.Services.Common exposing (cleanPath, nameProperty, noPrep) 19 + import Time 20 + 21 + 22 + 23 + -- PROPERTIES 24 + -- 📟 25 + 26 + 27 + defaults = 28 + { directoryPath = "/" 29 + , name = "Music from Amazon S3" 30 + , region = "eu-west-1" 31 + } 32 + 33 + 34 + {-| The list of properties we need from the user. 35 + -} 36 + properties : List Property 37 + properties = 38 + [ { prop = "accessKey", labl = "Access key", plho = "Fv6EWfLfCcMo", pass = False } 39 + , { prop = "secretKey", labl = "Secret key", plho = "qeNcqiMpgqC8", pass = True } 40 + , { prop = "bucketName", labl = "Bucket name", plho = "music", pass = False } 41 + , { prop = "region", labl = "Region", plho = defaults.region, pass = False } 42 + , { prop = "directoryPath", labl = "Directory", plho = defaults.directoryPath, pass = False } 43 + , { prop = "host", labl = "Host (optional)", plho = "http://127.0.0.1:9000", pass = False } 44 + 45 + -- 46 + , nameProperty defaults.name 47 + ] 48 + 49 + 50 + {-| Initial data set. 51 + -} 52 + initialData : SourceData 53 + initialData = 54 + Dict.fromList 55 + [ ( "accessKey", "" ) 56 + , ( "bucketName", "" ) 57 + , ( "directoryPath", defaults.directoryPath ) 58 + , ( "host", "" ) 59 + , ( "name", defaults.name ) 60 + , ( "region", defaults.region ) 61 + , ( "secretKey", "" ) 62 + ] 63 + 64 + 65 + 66 + -- PREPARATION 67 + 68 + 69 + prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) 70 + prepare _ _ _ _ = 71 + Nothing 72 + 73 + 74 + 75 + -- TREE 76 + 77 + 78 + {-| Create a directory tree. 79 + 80 + List all the tracks in the bucket. 81 + Or a specific directory in the bucket. 82 + 83 + -} 84 + makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg 85 + makeTree srcData marker currentTime resultMsg = 86 + let 87 + directoryPath = 88 + srcData 89 + |> Dict.get "directoryPath" 90 + |> Maybe.withDefault defaults.directoryPath 91 + |> cleanPath 92 + 93 + initialParams = 94 + [ ( "list-type", "2" ) 95 + , ( "max-keys", "1000" ) 96 + ] 97 + 98 + prefix = 99 + if String.length directoryPath > 0 then 100 + [ ( "prefix", directoryPath ) ] 101 + 102 + else 103 + [] 104 + 105 + continuation = 106 + case marker of 107 + InProgress s -> 108 + [ ( "continuation-token", s ) ] 109 + 110 + _ -> 111 + [] 112 + 113 + params = 114 + initialParams ++ prefix ++ continuation 115 + 116 + url = 117 + presignedUrl Get (60 * 5) params currentTime srcData "/" 118 + in 119 + Http.get 120 + { url = url 121 + , expect = Http.expectString resultMsg 122 + } 123 + 124 + 125 + {-| Re-export parser functions. 126 + -} 127 + parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 128 + parsePreparationResponse = 129 + noPrep 130 + 131 + 132 + parseTreeResponse : String -> Marker -> TreeAnswer Marker 133 + parseTreeResponse = 134 + Parser.parseTreeResponse 135 + 136 + 137 + parseErrorResponse : String -> String 138 + parseErrorResponse = 139 + Parser.parseErrorResponse 140 + 141 + 142 + 143 + -- Post 144 + 145 + 146 + {-| Post process the tree results. 147 + 148 + Make sure we only use music files that we can use. 149 + 150 + -} 151 + postProcessTree : List String -> List String 152 + postProcessTree = 153 + Sources.Pick.selectMusicFiles 154 + 155 + 156 + 157 + -- Track URL 158 + 159 + 160 + {-| Create a public url for a file. 161 + 162 + We need this to play the track. 163 + Creates a presigned url that's valid for 48 hours 164 + 165 + -} 166 + makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 167 + makeTrackUrl currentTime srcData method pathToFile = 168 + presignedUrl method 172800 [] currentTime srcData pathToFile
+62
src/Library/Sources/Services/AmazonS3/Parser.elm
··· 1 + module Sources.Services.AmazonS3.Parser exposing (parseErrorResponse, parseTreeResponse) 2 + 3 + import Conditional exposing (..) 4 + import Sources.Processing exposing (Marker(..), TreeAnswer) 5 + import Xml.Decode exposing (..) 6 + 7 + 8 + 9 + -- TREE 10 + 11 + 12 + parseTreeResponse : String -> Marker -> TreeAnswer Marker 13 + parseTreeResponse response _ = 14 + response 15 + |> decodeString 16 + (map2 17 + (\f m -> { filePaths = f, marker = m }) 18 + filePathsDecoder 19 + markerDecoder 20 + ) 21 + |> Result.withDefault { filePaths = [], marker = TheEnd } 22 + 23 + 24 + filePathsDecoder : Decoder (List String) 25 + filePathsDecoder = 26 + path [ "Contents" ] (list <| path [ "Key" ] (single string)) 27 + 28 + 29 + markerDecoder : Decoder Marker 30 + markerDecoder = 31 + map2 32 + (\a b -> 33 + Maybe.withDefault 34 + TheEnd 35 + (Maybe.map2 36 + (\isTruncated token -> 37 + ifThenElse (isTruncated == "true") (InProgress token) TheEnd 38 + ) 39 + a 40 + b 41 + ) 42 + ) 43 + (maybe <| path [ "IsTruncated" ] (single string)) 44 + (maybe <| path [ "NextContinuationToken" ] (single string)) 45 + 46 + 47 + 48 + -- ERROR 49 + 50 + 51 + parseErrorResponse : String -> String 52 + parseErrorResponse response = 53 + response 54 + |> decodeString errorMessagesDecoder 55 + |> Result.toMaybe 56 + |> Maybe.andThen List.head 57 + |> Maybe.withDefault "Invalid request" 58 + 59 + 60 + errorMessagesDecoder : Decoder (List String) 61 + errorMessagesDecoder = 62 + path [ "Error" ] (list <| path [ "Message" ] (single string))
+172
src/Library/Sources/Services/AmazonS3/Presign.elm
··· 1 + module Sources.Services.AmazonS3.Presign exposing (presignedUrl) 2 + 3 + import Binary 4 + import Crypto.Binary as Binary 5 + import Crypto.HMAC as HMAC 6 + import DateFormat as Date 7 + import Dict 8 + import Dict.Ext as Dict 9 + import Maybe.Extra as Maybe 10 + import SHA 11 + import Sources exposing (SourceData) 12 + import Sources.Processing exposing (HttpMethod, httpMethod) 13 + import String.Ext as String 14 + import Time 15 + import Url 16 + import Url.Builder as Url 17 + 18 + 19 + presignedUrl : 20 + HttpMethod 21 + -> Int 22 + -> List ( String, String ) 23 + -> Time.Posix 24 + -> SourceData 25 + -> String 26 + -> String 27 + presignedUrl method lifeExpectancyInSeconds extraParams currentTime srcData pathToFile = 28 + let 29 + aws = 30 + srcData 31 + 32 + region = 33 + Dict.fetchUnknown "region" aws 34 + 35 + bucketName = 36 + Dict.fetchUnknown "bucketName" aws 37 + 38 + customHost = 39 + Dict.get "host" aws 40 + 41 + host = 42 + case customHost of 43 + Just h -> 44 + h 45 + |> String.chopStart "http://" 46 + |> String.chopStart "https://" 47 + |> String.chopEnd "/" 48 + 49 + Nothing -> 50 + bucketName ++ ".s3.amazonaws.com" 51 + 52 + protocol = 53 + if String.contains "http://" (Maybe.withDefault "" customHost) then 54 + "http://" 55 + 56 + else 57 + "https://" 58 + 59 + -- {var} Paths 60 + filePathPrefix = 61 + if Maybe.isJust customHost then 62 + "/" ++ bucketName 63 + 64 + else 65 + "" 66 + 67 + filePath = 68 + pathToFile 69 + |> String.chopStart "/" 70 + |> String.split "/" 71 + |> List.map Url.percentEncode 72 + |> String.join "/" 73 + |> String.append ("/" ++ filePathPrefix) 74 + 75 + -- {var} Time 76 + -- timestamp -> 20130721T201207Z 77 + -- date -> 20130721 78 + timestamp = 79 + Date.format 80 + [ Date.yearNumber 81 + , Date.monthFixed 82 + , Date.dayOfMonthFixed 83 + , Date.text "T" 84 + , Date.hourMilitaryFixed 85 + , Date.minuteFixed 86 + , Date.secondFixed 87 + , Date.text "Z" 88 + ] 89 + Time.utc 90 + currentTime 91 + 92 + date = 93 + Date.format 94 + [ Date.yearNumber 95 + , Date.monthFixed 96 + , Date.dayOfMonthFixed 97 + ] 98 + Time.utc 99 + currentTime 100 + 101 + -- Request 102 + credential = 103 + [ Dict.fetchUnknown "accessKey" aws 104 + , date 105 + , region 106 + , "s3" 107 + , "aws4_request" 108 + ] 109 + |> String.join "/" 110 + 111 + queryString = 112 + [ ( "X-Amz-Algorithm", "AWS4-HMAC-SHA256" ) 113 + , ( "X-Amz-Credential", credential ) 114 + , ( "X-Amz-Date", timestamp ) 115 + , ( "X-Amz-Expires", String.fromInt lifeExpectancyInSeconds ) 116 + , ( "X-Amz-SignedHeaders", "host" ) 117 + ] 118 + |> List.append extraParams 119 + |> List.sortBy Tuple.first 120 + |> List.map (\( a, b ) -> Url.string a b) 121 + |> Url.toQuery 122 + 123 + request = 124 + String.join 125 + "\n" 126 + [ httpMethod method 127 + , filePath 128 + , queryString 129 + , "host:" ++ host 130 + , "" 131 + , "host" 132 + , "UNSIGNED-PAYLOAD" 133 + ] 134 + 135 + -- String to sign 136 + stringToSign = 137 + String.join 138 + "\n" 139 + [ "AWS4-HMAC-SHA256" 140 + , timestamp 141 + , String.join "/" [ date, region, "s3", "aws4_request" ] 142 + , Binary.toHex (SHA.sha256 request) 143 + ] 144 + 145 + -- Signature 146 + signature = 147 + ("AWS4" ++ Dict.fetchUnknown "secretKey" aws) 148 + |> hmacSha256 date 149 + |> hmacSha256 region 150 + |> hmacSha256 "s3" 151 + |> hmacSha256 "aws4_request" 152 + |> hmacSha256 stringToSign 153 + in 154 + String.concat 155 + [ protocol 156 + , host 157 + , filePath 158 + , queryString 159 + , "&X-Amz-Signature=" 160 + , signature 161 + ] 162 + 163 + 164 + 165 + -- ⚗️ 166 + 167 + 168 + hmacSha256 : String -> String -> String 169 + hmacSha256 message key = 170 + key 171 + |> HMAC.encrypt64 SHA.sha256 message 172 + |> Binary.toString
+61
src/Library/Sources/Services/Common.elm
··· 1 + module Sources.Services.Common exposing (cleanPath, nameProperty, noPrep) 2 + 3 + import Sources exposing (..) 4 + import Sources.Processing exposing (..) 5 + import String.Ext as String 6 + 7 + 8 + 9 + -- FORMS 10 + 11 + 12 + nameProperty : String -> Property 13 + nameProperty placeholder = 14 + { prop = "name" 15 + , labl = "Name" 16 + , plho = placeholder 17 + , pass = False 18 + } 19 + 20 + 21 + 22 + -- PATHS 23 + 24 + 25 + {-| Clean a path. 26 + 27 + >>> cleanPath " " 28 + "" 29 + 30 + >>> cleanPath "/example" 31 + "example/" 32 + 33 + >>> cleanPath "example" 34 + "example/" 35 + 36 + >>> cleanPath "example/" 37 + "example/" 38 + 39 + -} 40 + cleanPath : String -> String 41 + cleanPath dirtyPath = 42 + dirtyPath 43 + |> String.trim 44 + |> String.chopStart "/" 45 + |> String.chopEnd "/" 46 + |> (\p -> 47 + if String.isEmpty p then 48 + p 49 + 50 + else 51 + p ++ "/" 52 + ) 53 + 54 + 55 + 56 + -- PARSING 57 + 58 + 59 + noPrep : String -> SourceData -> Marker -> PrepationAnswer Marker 60 + noPrep _ srcData _ = 61 + { sourceData = srcData, marker = TheEnd }
+19
src/Library/String/Ext.elm
··· 1 + module String.Ext exposing (chopEnd, chopStart) 2 + 3 + 4 + chopEnd : String -> String -> String 5 + chopEnd needle str = 6 + if String.endsWith needle str then 7 + String.dropRight (String.length str) str 8 + 9 + else 10 + str 11 + 12 + 13 + chopStart : String -> String -> String 14 + chopStart needle str = 15 + if String.startsWith needle str then 16 + String.dropLeft (String.length str) str 17 + 18 + else 19 + str