├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── filesystem ├── mod.rs ├── node.rs └── nodecache.rs ├── id3tag.rs ├── ioutil ├── concat.rs ├── lazyopen.rs ├── mod.rs ├── oprecorder.rs ├── pattern.rs ├── readseek.rs └── skip.rs ├── main.rs ├── mapping.rs ├── mp3.rs └── soundcloud ├── error.rs ├── format.rs ├── mod.rs ├── track.rs ├── user.rs └── util ├── http.rs └── mod.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | # schedule: 6 | # - cron: '0 0 * * 1' # weekly 7 | 8 | jobs: 9 | 10 | build: 11 | strategy: 12 | matrix: 13 | toolchain: [stable, nightly] 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | 20 | - name: Install dependencies 21 | run: sudo apt install libfuse-dev 22 | 23 | - name: Select channel 24 | run: rustup default ${{ matrix.toolchain }} 25 | 26 | - name: Update toolchain 27 | run: rustup update 28 | 29 | - name: Build 30 | run: cargo build 31 | 32 | - name: Test 33 | run: cargo test 34 | 35 | style: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v1 40 | 41 | - name: Install dependencies 42 | run: sudo apt install libfuse-dev 43 | 44 | - name: Rustfmt 45 | run: cargo fmt -- --check 46 | 47 | - name: Clippy 48 | run: cargo clippy 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "adler32" 5 | version = "1.0.4" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" 8 | 9 | [[package]] 10 | name = "aho-corasick" 11 | version = "0.7.9" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec" 14 | dependencies = [ 15 | "memchr", 16 | ] 17 | 18 | [[package]] 19 | name = "ansi_term" 20 | version = "0.11.0" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 23 | dependencies = [ 24 | "winapi 0.3.8", 25 | ] 26 | 27 | [[package]] 28 | name = "anyhow" 29 | version = "1.0.26" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" 32 | 33 | [[package]] 34 | name = "atty" 35 | version = "0.2.14" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 38 | dependencies = [ 39 | "hermit-abi", 40 | "libc", 41 | "winapi 0.3.8", 42 | ] 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 49 | 50 | [[package]] 51 | name = "base64" 52 | version = "0.11.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.2.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 61 | 62 | [[package]] 63 | name = "bumpalo" 64 | version = "3.2.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" 67 | 68 | [[package]] 69 | name = "byteorder" 70 | version = "1.3.4" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 73 | 74 | [[package]] 75 | name = "bytes" 76 | version = "0.5.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" 79 | 80 | [[package]] 81 | name = "c2-chacha" 82 | version = "0.2.3" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" 85 | dependencies = [ 86 | "ppv-lite86", 87 | ] 88 | 89 | [[package]] 90 | name = "cc" 91 | version = "1.0.50" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 94 | 95 | [[package]] 96 | name = "cfg-if" 97 | version = "0.1.10" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 100 | 101 | [[package]] 102 | name = "chrono" 103 | version = "0.4.10" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" 106 | dependencies = [ 107 | "num-integer", 108 | "num-traits", 109 | "time", 110 | ] 111 | 112 | [[package]] 113 | name = "clap" 114 | version = "2.33.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 117 | dependencies = [ 118 | "ansi_term", 119 | "atty", 120 | "bitflags", 121 | "strsim", 122 | "textwrap", 123 | "unicode-width", 124 | "vec_map", 125 | ] 126 | 127 | [[package]] 128 | name = "core-foundation" 129 | version = "0.6.4" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" 132 | dependencies = [ 133 | "core-foundation-sys", 134 | "libc", 135 | ] 136 | 137 | [[package]] 138 | name = "core-foundation-sys" 139 | version = "0.6.2" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" 142 | 143 | [[package]] 144 | name = "crc32fast" 145 | version = "1.2.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 148 | dependencies = [ 149 | "cfg-if", 150 | ] 151 | 152 | [[package]] 153 | name = "crossbeam-deque" 154 | version = "0.7.3" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" 157 | dependencies = [ 158 | "crossbeam-epoch", 159 | "crossbeam-utils", 160 | "maybe-uninit", 161 | ] 162 | 163 | [[package]] 164 | name = "crossbeam-epoch" 165 | version = "0.8.2" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" 168 | dependencies = [ 169 | "autocfg", 170 | "cfg-if", 171 | "crossbeam-utils", 172 | "lazy_static", 173 | "maybe-uninit", 174 | "memoffset", 175 | "scopeguard", 176 | ] 177 | 178 | [[package]] 179 | name = "crossbeam-queue" 180 | version = "0.2.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" 183 | dependencies = [ 184 | "cfg-if", 185 | "crossbeam-utils", 186 | ] 187 | 188 | [[package]] 189 | name = "crossbeam-utils" 190 | version = "0.7.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 193 | dependencies = [ 194 | "autocfg", 195 | "cfg-if", 196 | "lazy_static", 197 | ] 198 | 199 | [[package]] 200 | name = "dtoa" 201 | version = "0.4.5" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" 204 | 205 | [[package]] 206 | name = "either" 207 | version = "1.5.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 210 | 211 | [[package]] 212 | name = "encoding" 213 | version = "0.2.33" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" 216 | dependencies = [ 217 | "encoding-index-japanese", 218 | "encoding-index-korean", 219 | "encoding-index-simpchinese", 220 | "encoding-index-singlebyte", 221 | "encoding-index-tradchinese", 222 | ] 223 | 224 | [[package]] 225 | name = "encoding-index-japanese" 226 | version = "1.20141219.5" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" 229 | dependencies = [ 230 | "encoding_index_tests", 231 | ] 232 | 233 | [[package]] 234 | name = "encoding-index-korean" 235 | version = "1.20141219.5" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" 238 | dependencies = [ 239 | "encoding_index_tests", 240 | ] 241 | 242 | [[package]] 243 | name = "encoding-index-simpchinese" 244 | version = "1.20141219.5" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" 247 | dependencies = [ 248 | "encoding_index_tests", 249 | ] 250 | 251 | [[package]] 252 | name = "encoding-index-singlebyte" 253 | version = "1.20141219.5" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" 256 | dependencies = [ 257 | "encoding_index_tests", 258 | ] 259 | 260 | [[package]] 261 | name = "encoding-index-tradchinese" 262 | version = "1.20141219.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" 265 | dependencies = [ 266 | "encoding_index_tests", 267 | ] 268 | 269 | [[package]] 270 | name = "encoding_index_tests" 271 | version = "0.1.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" 274 | 275 | [[package]] 276 | name = "encoding_rs" 277 | version = "0.8.22" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" 280 | dependencies = [ 281 | "cfg-if", 282 | ] 283 | 284 | [[package]] 285 | name = "env_logger" 286 | version = "0.7.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 289 | dependencies = [ 290 | "atty", 291 | "humantime", 292 | "log 0.4.8", 293 | "regex", 294 | "termcolor", 295 | ] 296 | 297 | [[package]] 298 | name = "flate2" 299 | version = "1.0.13" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" 302 | dependencies = [ 303 | "cfg-if", 304 | "crc32fast", 305 | "libc", 306 | "miniz_oxide", 307 | ] 308 | 309 | [[package]] 310 | name = "fnv" 311 | version = "1.0.6" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 314 | 315 | [[package]] 316 | name = "foreign-types" 317 | version = "0.3.2" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 320 | dependencies = [ 321 | "foreign-types-shared", 322 | ] 323 | 324 | [[package]] 325 | name = "foreign-types-shared" 326 | version = "0.1.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 329 | 330 | [[package]] 331 | name = "fuchsia-zircon" 332 | version = "0.3.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 335 | dependencies = [ 336 | "bitflags", 337 | "fuchsia-zircon-sys", 338 | ] 339 | 340 | [[package]] 341 | name = "fuchsia-zircon-sys" 342 | version = "0.3.3" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 345 | 346 | [[package]] 347 | name = "fuse" 348 | version = "0.3.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "80e57070510966bfef93662a81cb8aa2b1c7db0964354fa9921434f04b9e8660" 351 | dependencies = [ 352 | "libc", 353 | "log 0.3.9", 354 | "pkg-config", 355 | "thread-scoped", 356 | "time", 357 | ] 358 | 359 | [[package]] 360 | name = "futures-channel" 361 | version = "0.3.4" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" 364 | dependencies = [ 365 | "futures-core", 366 | ] 367 | 368 | [[package]] 369 | name = "futures-core" 370 | version = "0.3.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" 373 | 374 | [[package]] 375 | name = "futures-io" 376 | version = "0.3.4" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" 379 | 380 | [[package]] 381 | name = "futures-sink" 382 | version = "0.3.4" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" 385 | 386 | [[package]] 387 | name = "futures-task" 388 | version = "0.3.4" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" 391 | 392 | [[package]] 393 | name = "futures-util" 394 | version = "0.3.4" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" 397 | dependencies = [ 398 | "futures-core", 399 | "futures-io", 400 | "futures-task", 401 | "memchr", 402 | "pin-utils", 403 | "slab", 404 | ] 405 | 406 | [[package]] 407 | name = "getrandom" 408 | version = "0.1.14" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 411 | dependencies = [ 412 | "cfg-if", 413 | "libc", 414 | "wasi", 415 | ] 416 | 417 | [[package]] 418 | name = "h2" 419 | version = "0.2.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1" 422 | dependencies = [ 423 | "bytes", 424 | "fnv", 425 | "futures-core", 426 | "futures-sink", 427 | "futures-util", 428 | "http", 429 | "indexmap", 430 | "log 0.4.8", 431 | "slab", 432 | "tokio", 433 | "tokio-util", 434 | ] 435 | 436 | [[package]] 437 | name = "heck" 438 | version = "0.3.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 441 | dependencies = [ 442 | "unicode-segmentation", 443 | ] 444 | 445 | [[package]] 446 | name = "hermit-abi" 447 | version = "0.1.8" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" 450 | dependencies = [ 451 | "libc", 452 | ] 453 | 454 | [[package]] 455 | name = "http" 456 | version = "0.2.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" 459 | dependencies = [ 460 | "bytes", 461 | "fnv", 462 | "itoa", 463 | ] 464 | 465 | [[package]] 466 | name = "http-body" 467 | version = "0.3.1" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" 470 | dependencies = [ 471 | "bytes", 472 | "http", 473 | ] 474 | 475 | [[package]] 476 | name = "httparse" 477 | version = "1.3.4" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" 480 | 481 | [[package]] 482 | name = "humantime" 483 | version = "1.3.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 486 | dependencies = [ 487 | "quick-error", 488 | ] 489 | 490 | [[package]] 491 | name = "hyper" 492 | version = "0.13.2" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "fa1c527bbc634be72aa7ba31e4e4def9bbb020f5416916279b7c705cd838893e" 495 | dependencies = [ 496 | "bytes", 497 | "futures-channel", 498 | "futures-core", 499 | "futures-util", 500 | "h2", 501 | "http", 502 | "http-body", 503 | "httparse", 504 | "itoa", 505 | "log 0.4.8", 506 | "net2", 507 | "pin-project", 508 | "time", 509 | "tokio", 510 | "tower-service", 511 | "want", 512 | ] 513 | 514 | [[package]] 515 | name = "hyper-tls" 516 | version = "0.4.1" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" 519 | dependencies = [ 520 | "bytes", 521 | "hyper", 522 | "native-tls", 523 | "tokio", 524 | "tokio-tls", 525 | ] 526 | 527 | [[package]] 528 | name = "id3" 529 | version = "0.5.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "dd92391934974b5ceb7506c77897bdf95b632ac8ef10f2fee844b11f3674c624" 532 | dependencies = [ 533 | "bitflags", 534 | "byteorder", 535 | "encoding", 536 | "flate2", 537 | "lazy_static", 538 | ] 539 | 540 | [[package]] 541 | name = "idna" 542 | version = "0.2.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 545 | dependencies = [ 546 | "matches", 547 | "unicode-bidi", 548 | "unicode-normalization", 549 | ] 550 | 551 | [[package]] 552 | name = "indexmap" 553 | version = "1.3.2" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" 556 | dependencies = [ 557 | "autocfg", 558 | ] 559 | 560 | [[package]] 561 | name = "iovec" 562 | version = "0.1.4" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 565 | dependencies = [ 566 | "libc", 567 | ] 568 | 569 | [[package]] 570 | name = "itoa" 571 | version = "0.4.5" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 574 | 575 | [[package]] 576 | name = "js-sys" 577 | version = "0.3.35" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" 580 | dependencies = [ 581 | "wasm-bindgen", 582 | ] 583 | 584 | [[package]] 585 | name = "kernel32-sys" 586 | version = "0.2.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 589 | dependencies = [ 590 | "winapi 0.2.8", 591 | "winapi-build", 592 | ] 593 | 594 | [[package]] 595 | name = "lazy_static" 596 | version = "1.4.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 599 | 600 | [[package]] 601 | name = "libc" 602 | version = "0.2.67" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" 605 | 606 | [[package]] 607 | name = "log" 608 | version = "0.3.9" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 611 | dependencies = [ 612 | "log 0.4.8", 613 | ] 614 | 615 | [[package]] 616 | name = "log" 617 | version = "0.4.8" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 620 | dependencies = [ 621 | "cfg-if", 622 | ] 623 | 624 | [[package]] 625 | name = "matches" 626 | version = "0.1.8" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 629 | 630 | [[package]] 631 | name = "maybe-uninit" 632 | version = "2.0.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 635 | 636 | [[package]] 637 | name = "memchr" 638 | version = "2.3.3" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 641 | 642 | [[package]] 643 | name = "memoffset" 644 | version = "0.5.3" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" 647 | dependencies = [ 648 | "rustc_version", 649 | ] 650 | 651 | [[package]] 652 | name = "mime" 653 | version = "0.3.16" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 656 | 657 | [[package]] 658 | name = "mime_guess" 659 | version = "2.0.1" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" 662 | dependencies = [ 663 | "mime", 664 | "unicase", 665 | ] 666 | 667 | [[package]] 668 | name = "miniz_oxide" 669 | version = "0.3.6" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" 672 | dependencies = [ 673 | "adler32", 674 | ] 675 | 676 | [[package]] 677 | name = "mio" 678 | version = "0.6.21" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" 681 | dependencies = [ 682 | "cfg-if", 683 | "fuchsia-zircon", 684 | "fuchsia-zircon-sys", 685 | "iovec", 686 | "kernel32-sys", 687 | "libc", 688 | "log 0.4.8", 689 | "miow", 690 | "net2", 691 | "slab", 692 | "winapi 0.2.8", 693 | ] 694 | 695 | [[package]] 696 | name = "miow" 697 | version = "0.2.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" 700 | dependencies = [ 701 | "kernel32-sys", 702 | "net2", 703 | "winapi 0.2.8", 704 | "ws2_32-sys", 705 | ] 706 | 707 | [[package]] 708 | name = "native-tls" 709 | version = "0.2.3" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" 712 | dependencies = [ 713 | "lazy_static", 714 | "libc", 715 | "log 0.4.8", 716 | "openssl", 717 | "openssl-probe", 718 | "openssl-sys", 719 | "schannel", 720 | "security-framework", 721 | "security-framework-sys", 722 | "tempfile", 723 | ] 724 | 725 | [[package]] 726 | name = "net2" 727 | version = "0.2.33" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" 730 | dependencies = [ 731 | "cfg-if", 732 | "libc", 733 | "winapi 0.3.8", 734 | ] 735 | 736 | [[package]] 737 | name = "nix" 738 | version = "0.16.1" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "dd0eaf8df8bab402257e0a5c17a254e4cc1f72a93588a1ddfb5d356c801aa7cb" 741 | dependencies = [ 742 | "bitflags", 743 | "cc", 744 | "cfg-if", 745 | "libc", 746 | "void", 747 | ] 748 | 749 | [[package]] 750 | name = "nom" 751 | version = "4.2.3" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 754 | dependencies = [ 755 | "memchr", 756 | "version_check 0.1.5", 757 | ] 758 | 759 | [[package]] 760 | name = "num-integer" 761 | version = "0.1.42" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 764 | dependencies = [ 765 | "autocfg", 766 | "num-traits", 767 | ] 768 | 769 | [[package]] 770 | name = "num-traits" 771 | version = "0.2.11" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 774 | dependencies = [ 775 | "autocfg", 776 | ] 777 | 778 | [[package]] 779 | name = "num_cpus" 780 | version = "1.12.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" 783 | dependencies = [ 784 | "hermit-abi", 785 | "libc", 786 | ] 787 | 788 | [[package]] 789 | name = "openssl" 790 | version = "0.10.28" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" 793 | dependencies = [ 794 | "bitflags", 795 | "cfg-if", 796 | "foreign-types", 797 | "lazy_static", 798 | "libc", 799 | "openssl-sys", 800 | ] 801 | 802 | [[package]] 803 | name = "openssl-probe" 804 | version = "0.1.2" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 807 | 808 | [[package]] 809 | name = "openssl-sys" 810 | version = "0.9.54" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" 813 | dependencies = [ 814 | "autocfg", 815 | "cc", 816 | "libc", 817 | "pkg-config", 818 | "vcpkg", 819 | ] 820 | 821 | [[package]] 822 | name = "percent-encoding" 823 | version = "2.1.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 826 | 827 | [[package]] 828 | name = "pin-project" 829 | version = "0.4.8" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" 832 | dependencies = [ 833 | "pin-project-internal", 834 | ] 835 | 836 | [[package]] 837 | name = "pin-project-internal" 838 | version = "0.4.8" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" 841 | dependencies = [ 842 | "proc-macro2", 843 | "quote", 844 | "syn", 845 | ] 846 | 847 | [[package]] 848 | name = "pin-project-lite" 849 | version = "0.1.4" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" 852 | 853 | [[package]] 854 | name = "pin-utils" 855 | version = "0.1.0-alpha.4" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 858 | 859 | [[package]] 860 | name = "pkg-config" 861 | version = "0.3.17" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 864 | 865 | [[package]] 866 | name = "ppv-lite86" 867 | version = "0.2.6" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 870 | 871 | [[package]] 872 | name = "proc-macro2" 873 | version = "1.0.9" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" 876 | dependencies = [ 877 | "unicode-xid", 878 | ] 879 | 880 | [[package]] 881 | name = "quick-error" 882 | version = "1.2.3" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 885 | 886 | [[package]] 887 | name = "quote" 888 | version = "1.0.2" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 891 | dependencies = [ 892 | "proc-macro2", 893 | ] 894 | 895 | [[package]] 896 | name = "rand" 897 | version = "0.7.3" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 900 | dependencies = [ 901 | "getrandom", 902 | "libc", 903 | "rand_chacha", 904 | "rand_core", 905 | "rand_hc", 906 | ] 907 | 908 | [[package]] 909 | name = "rand_chacha" 910 | version = "0.2.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" 913 | dependencies = [ 914 | "c2-chacha", 915 | "rand_core", 916 | ] 917 | 918 | [[package]] 919 | name = "rand_core" 920 | version = "0.5.1" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 923 | dependencies = [ 924 | "getrandom", 925 | ] 926 | 927 | [[package]] 928 | name = "rand_hc" 929 | version = "0.2.0" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 932 | dependencies = [ 933 | "rand_core", 934 | ] 935 | 936 | [[package]] 937 | name = "rayon" 938 | version = "1.3.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" 941 | dependencies = [ 942 | "crossbeam-deque", 943 | "either", 944 | "rayon-core", 945 | ] 946 | 947 | [[package]] 948 | name = "rayon-core" 949 | version = "1.7.0" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" 952 | dependencies = [ 953 | "crossbeam-deque", 954 | "crossbeam-queue", 955 | "crossbeam-utils", 956 | "lazy_static", 957 | "num_cpus", 958 | ] 959 | 960 | [[package]] 961 | name = "redox_syscall" 962 | version = "0.1.56" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 965 | 966 | [[package]] 967 | name = "regex" 968 | version = "1.3.4" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" 971 | dependencies = [ 972 | "aho-corasick", 973 | "memchr", 974 | "regex-syntax", 975 | "thread_local", 976 | ] 977 | 978 | [[package]] 979 | name = "regex-syntax" 980 | version = "0.6.14" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "b28dfe3fe9badec5dbf0a79a9cccad2cfc2ab5484bdb3e44cbd1ae8b3ba2be06" 983 | 984 | [[package]] 985 | name = "remove_dir_all" 986 | version = "0.5.2" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 989 | dependencies = [ 990 | "winapi 0.3.8", 991 | ] 992 | 993 | [[package]] 994 | name = "reqwest" 995 | version = "0.10.3" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "a9f62f24514117d09a8fc74b803d3d65faa27cea1c7378fb12b0d002913f3831" 998 | dependencies = [ 999 | "base64", 1000 | "bytes", 1001 | "encoding_rs", 1002 | "futures-core", 1003 | "futures-util", 1004 | "http", 1005 | "http-body", 1006 | "hyper", 1007 | "hyper-tls", 1008 | "js-sys", 1009 | "lazy_static", 1010 | "log 0.4.8", 1011 | "mime", 1012 | "mime_guess", 1013 | "native-tls", 1014 | "percent-encoding", 1015 | "pin-project-lite", 1016 | "serde", 1017 | "serde_json", 1018 | "serde_urlencoded", 1019 | "time", 1020 | "tokio", 1021 | "tokio-tls", 1022 | "url", 1023 | "wasm-bindgen", 1024 | "wasm-bindgen-futures", 1025 | "web-sys", 1026 | "winreg", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "rustc_version" 1031 | version = "0.2.3" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 1034 | dependencies = [ 1035 | "semver", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "ryu" 1040 | version = "1.0.2" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 1043 | 1044 | [[package]] 1045 | name = "schannel" 1046 | version = "0.1.17" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "507a9e6e8ffe0a4e0ebb9a10293e62fdf7657c06f1b8bb07a8fcf697d2abf295" 1049 | dependencies = [ 1050 | "lazy_static", 1051 | "winapi 0.3.8", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "scopeguard" 1056 | version = "1.1.0" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1059 | 1060 | [[package]] 1061 | name = "security-framework" 1062 | version = "0.3.4" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df" 1065 | dependencies = [ 1066 | "core-foundation", 1067 | "core-foundation-sys", 1068 | "libc", 1069 | "security-framework-sys", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "security-framework-sys" 1074 | version = "0.3.3" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895" 1077 | dependencies = [ 1078 | "core-foundation-sys", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "semver" 1083 | version = "0.9.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 1086 | dependencies = [ 1087 | "semver-parser", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "semver-parser" 1092 | version = "0.7.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 1095 | 1096 | [[package]] 1097 | name = "serde" 1098 | version = "1.0.104" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 1101 | 1102 | [[package]] 1103 | name = "serde_derive" 1104 | version = "1.0.104" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 1107 | dependencies = [ 1108 | "proc-macro2", 1109 | "quote", 1110 | "syn", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "serde_json" 1115 | version = "1.0.48" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" 1118 | dependencies = [ 1119 | "itoa", 1120 | "ryu", 1121 | "serde", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "serde_urlencoded" 1126 | version = "0.6.1" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" 1129 | dependencies = [ 1130 | "dtoa", 1131 | "itoa", 1132 | "serde", 1133 | "url", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "slab" 1138 | version = "0.4.2" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1141 | 1142 | [[package]] 1143 | name = "smallvec" 1144 | version = "1.2.0" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 1147 | 1148 | [[package]] 1149 | name = "soundcloud-fs" 1150 | version = "0.2.0" 1151 | dependencies = [ 1152 | "chrono", 1153 | "clap", 1154 | "env_logger", 1155 | "fuse", 1156 | "id3", 1157 | "lazy_static", 1158 | "libc", 1159 | "log 0.4.8", 1160 | "nix", 1161 | "rayon", 1162 | "regex", 1163 | "reqwest", 1164 | "serde", 1165 | "serde_derive", 1166 | "serde_json", 1167 | "time", 1168 | "url", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "sourcefile" 1173 | version = "0.1.4" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" 1176 | 1177 | [[package]] 1178 | name = "strsim" 1179 | version = "0.8.0" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1182 | 1183 | [[package]] 1184 | name = "syn" 1185 | version = "1.0.16" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" 1188 | dependencies = [ 1189 | "proc-macro2", 1190 | "quote", 1191 | "unicode-xid", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "tempfile" 1196 | version = "3.1.0" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 1199 | dependencies = [ 1200 | "cfg-if", 1201 | "libc", 1202 | "rand", 1203 | "redox_syscall", 1204 | "remove_dir_all", 1205 | "winapi 0.3.8", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "termcolor" 1210 | version = "1.1.0" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 1213 | dependencies = [ 1214 | "winapi-util", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "textwrap" 1219 | version = "0.11.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1222 | dependencies = [ 1223 | "unicode-width", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "thread-scoped" 1228 | version = "1.0.2" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "bcbb6aa301e5d3b0b5ef639c9a9c7e2f1c944f177b460c04dc24c69b1fa2bd99" 1231 | 1232 | [[package]] 1233 | name = "thread_local" 1234 | version = "1.0.1" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 1237 | dependencies = [ 1238 | "lazy_static", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "time" 1243 | version = "0.1.42" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 1246 | dependencies = [ 1247 | "libc", 1248 | "redox_syscall", 1249 | "winapi 0.3.8", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "tokio" 1254 | version = "0.2.12" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "b34bee1facdc352fba10c9c58b654e6ecb6a2250167772bf86071f7c5f2f5061" 1257 | dependencies = [ 1258 | "bytes", 1259 | "fnv", 1260 | "iovec", 1261 | "lazy_static", 1262 | "memchr", 1263 | "mio", 1264 | "num_cpus", 1265 | "pin-project-lite", 1266 | "slab", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "tokio-tls" 1271 | version = "0.3.0" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" 1274 | dependencies = [ 1275 | "native-tls", 1276 | "tokio", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "tokio-util" 1281 | version = "0.2.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" 1284 | dependencies = [ 1285 | "bytes", 1286 | "futures-core", 1287 | "futures-sink", 1288 | "log 0.4.8", 1289 | "pin-project-lite", 1290 | "tokio", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "tower-service" 1295 | version = "0.3.0" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" 1298 | 1299 | [[package]] 1300 | name = "try-lock" 1301 | version = "0.2.2" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" 1304 | 1305 | [[package]] 1306 | name = "unicase" 1307 | version = "2.6.0" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1310 | dependencies = [ 1311 | "version_check 0.9.1", 1312 | ] 1313 | 1314 | [[package]] 1315 | name = "unicode-bidi" 1316 | version = "0.3.4" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 1319 | dependencies = [ 1320 | "matches", 1321 | ] 1322 | 1323 | [[package]] 1324 | name = "unicode-normalization" 1325 | version = "0.1.12" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 1328 | dependencies = [ 1329 | "smallvec", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "unicode-segmentation" 1334 | version = "1.6.0" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 1337 | 1338 | [[package]] 1339 | name = "unicode-width" 1340 | version = "0.1.7" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 1343 | 1344 | [[package]] 1345 | name = "unicode-xid" 1346 | version = "0.2.0" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 1349 | 1350 | [[package]] 1351 | name = "url" 1352 | version = "2.1.1" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 1355 | dependencies = [ 1356 | "idna", 1357 | "matches", 1358 | "percent-encoding", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "vcpkg" 1363 | version = "0.2.8" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" 1366 | 1367 | [[package]] 1368 | name = "vec_map" 1369 | version = "0.8.1" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 1372 | 1373 | [[package]] 1374 | name = "version_check" 1375 | version = "0.1.5" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1378 | 1379 | [[package]] 1380 | name = "version_check" 1381 | version = "0.9.1" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 1384 | 1385 | [[package]] 1386 | name = "void" 1387 | version = "1.0.2" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 1390 | 1391 | [[package]] 1392 | name = "want" 1393 | version = "0.3.0" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1396 | dependencies = [ 1397 | "log 0.4.8", 1398 | "try-lock", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "wasi" 1403 | version = "0.9.0+wasi-snapshot-preview1" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1406 | 1407 | [[package]] 1408 | name = "wasm-bindgen" 1409 | version = "0.2.58" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" 1412 | dependencies = [ 1413 | "cfg-if", 1414 | "serde", 1415 | "serde_json", 1416 | "wasm-bindgen-macro", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "wasm-bindgen-backend" 1421 | version = "0.2.58" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" 1424 | dependencies = [ 1425 | "bumpalo", 1426 | "lazy_static", 1427 | "log 0.4.8", 1428 | "proc-macro2", 1429 | "quote", 1430 | "syn", 1431 | "wasm-bindgen-shared", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "wasm-bindgen-futures" 1436 | version = "0.4.8" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "8bbdd49e3e28b40dec6a9ba8d17798245ce32b019513a845369c641b275135d9" 1439 | dependencies = [ 1440 | "cfg-if", 1441 | "js-sys", 1442 | "wasm-bindgen", 1443 | "web-sys", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "wasm-bindgen-macro" 1448 | version = "0.2.58" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" 1451 | dependencies = [ 1452 | "quote", 1453 | "wasm-bindgen-macro-support", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "wasm-bindgen-macro-support" 1458 | version = "0.2.58" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" 1461 | dependencies = [ 1462 | "proc-macro2", 1463 | "quote", 1464 | "syn", 1465 | "wasm-bindgen-backend", 1466 | "wasm-bindgen-shared", 1467 | ] 1468 | 1469 | [[package]] 1470 | name = "wasm-bindgen-shared" 1471 | version = "0.2.58" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" 1474 | 1475 | [[package]] 1476 | name = "wasm-bindgen-webidl" 1477 | version = "0.2.58" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" 1480 | dependencies = [ 1481 | "anyhow", 1482 | "heck", 1483 | "log 0.4.8", 1484 | "proc-macro2", 1485 | "quote", 1486 | "syn", 1487 | "wasm-bindgen-backend", 1488 | "weedle", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "web-sys" 1493 | version = "0.3.35" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" 1496 | dependencies = [ 1497 | "anyhow", 1498 | "js-sys", 1499 | "sourcefile", 1500 | "wasm-bindgen", 1501 | "wasm-bindgen-webidl", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "weedle" 1506 | version = "0.10.0" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" 1509 | dependencies = [ 1510 | "nom", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "winapi" 1515 | version = "0.2.8" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 1518 | 1519 | [[package]] 1520 | name = "winapi" 1521 | version = "0.3.8" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 1524 | dependencies = [ 1525 | "winapi-i686-pc-windows-gnu", 1526 | "winapi-x86_64-pc-windows-gnu", 1527 | ] 1528 | 1529 | [[package]] 1530 | name = "winapi-build" 1531 | version = "0.1.1" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 1534 | 1535 | [[package]] 1536 | name = "winapi-i686-pc-windows-gnu" 1537 | version = "0.4.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1540 | 1541 | [[package]] 1542 | name = "winapi-util" 1543 | version = "0.1.3" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" 1546 | dependencies = [ 1547 | "winapi 0.3.8", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "winapi-x86_64-pc-windows-gnu" 1552 | version = "0.4.0" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1555 | 1556 | [[package]] 1557 | name = "winreg" 1558 | version = "0.6.2" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" 1561 | dependencies = [ 1562 | "winapi 0.3.8", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "ws2_32-sys" 1567 | version = "0.2.1" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 1570 | dependencies = [ 1571 | "winapi 0.2.8", 1572 | "winapi-build", 1573 | ] 1574 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soundcloud-fs" 3 | version = "0.2.0" 4 | authors = ["polyfloyd "] 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/polyfloyd/soundcloud-fs" 9 | description = "FUSE driver for SoundCloud" 10 | keywords = ["soundcloud", "fuse"] 11 | include = [ 12 | "src/**/*.rs", 13 | "Cargo.toml", 14 | "README.md", 15 | "LICENSE", 16 | ] 17 | 18 | [dependencies] 19 | chrono = "0.4" 20 | clap = "2" 21 | env_logger = "0.7" 22 | fuse = "0.3" 23 | id3 = "0.5" 24 | lazy_static = "1" 25 | libc = "0.2" 26 | log = "0.4" 27 | nix = "0.16" 28 | rayon = "1" 29 | regex = "1" 30 | reqwest = { version = "0.10", features = [ "blocking", "json" ] } 31 | serde = "1" 32 | serde_derive = "1" 33 | serde_json = "1" 34 | time = "0.1" 35 | url = "2" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | he MIT License (MIT) 2 | 3 | Copyright (c) 2019 polyfloyd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SoundCloud FS 2 | ============= 3 | 4 | This project is broken and no longer maintained. 5 | 6 | [![Build Status](https://github.com/polyfloyd/soundcloud-fs/workflows/CI/badge.svg)](https://github.com/polyfloyd/soundcloud-fs/actions) 7 | 8 | This project implements a FUSE driver to serve audio files from SoundCloud. It 9 | is optimized to avoid needless API requests and aid mass indexing by music 10 | libraries, specifically MPD. 11 | 12 | See also: https://polyfloyd.net/post/soundcloud-fuse-mpd/ 13 | 14 | ## Usage 15 | `soundcloud-fs --help` :) 16 | 17 | ## Notice 18 | This program is intended to be used as an interoperability layer for other 19 | software and not as a way to circumvent restrictions of the SoundCloud 20 | platform. When playing around with this program, I ask that you: 21 | 22 | * Do not mass-download content, consider the artists 23 | * Do not make excessive requests to the platform, consider the SoundCloud engineers 24 | -------------------------------------------------------------------------------- /src/filesystem/mod.rs: -------------------------------------------------------------------------------- 1 | mod node; 2 | mod nodecache; 3 | 4 | use chrono::{DateTime, Utc}; 5 | use fuse; 6 | use log::*; 7 | use std::collections::hash_map::DefaultHasher; 8 | use std::collections::HashMap; 9 | use std::ffi; 10 | use std::hash::{Hash, Hasher}; 11 | use std::io::{self, Read, Seek}; 12 | use std::os; 13 | use std::os::unix::ffi::OsStrExt; 14 | 15 | pub use self::node::*; 16 | pub use self::node::{Metadata, NodeType}; 17 | pub use self::nodecache::*; 18 | pub use crate::ioutil::*; 19 | 20 | const INO_ROOT: u64 = 1; 21 | 22 | pub struct FS 23 | where 24 | N: NodeType, 25 | { 26 | nodes: HashMap>, 27 | 28 | read_handles: HashMap::Reader>, 29 | next_read_handle: u64, 30 | 31 | readdir_handles: HashMap, u64)>>, 32 | next_readdir_handle: u64, 33 | 34 | uid: u32, 35 | gid: u32, 36 | } 37 | 38 | impl<'a, N> FS 39 | where 40 | N: NodeType, 41 | { 42 | pub fn new(root: &N, uid: u32, gid: u32) -> Self { 43 | let mut nodes = HashMap::new(); 44 | nodes.insert(INO_ROOT, Node::Directory(root.root())); 45 | FS { 46 | nodes, 47 | read_handles: HashMap::new(), 48 | next_read_handle: 1, 49 | readdir_handles: HashMap::new(), 50 | next_readdir_handle: 1, 51 | uid, 52 | gid, 53 | } 54 | } 55 | } 56 | 57 | impl fuse::Filesystem for FS 58 | where 59 | N: NodeType, 60 | { 61 | fn init(&mut self, _req: &fuse::Request) -> Result<(), os::raw::c_int> { 62 | trace!("fuse init"); 63 | Ok(()) 64 | } 65 | 66 | fn destroy(&mut self, _req: &fuse::Request) { 67 | trace!("fuse destroy"); 68 | } 69 | 70 | fn lookup( 71 | &mut self, 72 | _req: &fuse::Request, 73 | parent_ino: u64, 74 | os_name: &ffi::OsStr, 75 | reply: fuse::ReplyEntry, 76 | ) { 77 | let name = os_name.to_string_lossy(); 78 | trace!("fuse lookup, {}, {}", parent_ino, name); 79 | 80 | let child = { 81 | let parent = match self.nodes.get(&parent_ino) { 82 | Some(v) => v, 83 | None => { 84 | error!("fuse: no node for inode {}", parent_ino); 85 | reply.error(libc::ENOENT); 86 | return; 87 | } 88 | }; 89 | let dir = parent 90 | .directory() 91 | .expect("can not call lookup on a non-directory"); 92 | match dir.file_by_name(&name) { 93 | Ok(v) => v, 94 | Err(err) => { 95 | if err.errno() != libc::ENOENT { 96 | error!("fuse: could not get child {}: {}", name, err); 97 | } 98 | reply.error(err.errno()); 99 | return; 100 | } 101 | } 102 | }; 103 | 104 | let child_ino = inode_for_child(parent_ino, &name); 105 | 106 | let attrs = match attrs_for_file(&child, child_ino, self.uid, self.gid) { 107 | Ok(v) => v, 108 | Err(err) => { 109 | error!("fuse: can not get attrs for {}: {}", child_ino, err); 110 | reply.error(err.errno()); 111 | return; 112 | } 113 | }; 114 | 115 | self.nodes.insert(child_ino, child); 116 | 117 | let now = time::now().to_timespec(); 118 | reply.entry(&now, &attrs, 0); 119 | } 120 | 121 | fn getattr(&mut self, _req: &fuse::Request, ino: u64, reply: fuse::ReplyAttr) { 122 | trace!("fuse getattr: {}", ino); 123 | 124 | if let Some(node) = self.nodes.get(&ino) { 125 | let attrs = match attrs_for_file(&node, ino, self.uid, self.gid) { 126 | Ok(v) => v, 127 | Err(err) => { 128 | error!("fuse: can not get attrs for {}: {}", ino, err); 129 | reply.error(err.errno()); 130 | return; 131 | } 132 | }; 133 | let ttl = (time::now() + time::Duration::seconds(30)).to_timespec(); 134 | reply.attr(&ttl, &attrs); 135 | } else { 136 | reply.error(libc::ENOENT); 137 | } 138 | } 139 | 140 | fn readlink(&mut self, _req: &fuse::Request, ino: u64, reply: fuse::ReplyData) { 141 | trace!("fuse readlink: ino={}", ino); 142 | 143 | if let Some(node) = self.nodes.get(&ino) { 144 | let symlink = node 145 | .symlink() 146 | .expect("can not call readlink on a non-symlink"); 147 | let path = match symlink.read_link() { 148 | Ok(v) => v, 149 | Err(err) => { 150 | error!("fuse: could not read symlink: {}", err); 151 | reply.error(err.errno()); 152 | return; 153 | } 154 | }; 155 | reply.data(path.as_os_str().as_bytes()); 156 | } else { 157 | reply.error(libc::ENOENT); 158 | } 159 | } 160 | 161 | fn open(&mut self, _req: &fuse::Request, ino: u64, flags: u32, reply: fuse::ReplyOpen) { 162 | trace!("fuse open: {}, {:b}", ino, flags); 163 | 164 | const WRITE_FLAGS: i32 = libc::O_APPEND | libc::O_CREAT | libc::O_EXCL | libc::O_TRUNC; 165 | if flags & WRITE_FLAGS as u32 != 0 { 166 | error!("fuse: encountered write flag {:b}", flags); 167 | reply.error(libc::EROFS); 168 | return; 169 | } 170 | 171 | let node = match self.nodes.get(&ino) { 172 | Some(v) => v, 173 | None => { 174 | error!("fuse: no such inode: {}", ino); 175 | reply.error(libc::ENOENT); 176 | return; 177 | } 178 | }; 179 | let file = node.file().expect("can not call open on a non-file"); 180 | let reader = match file.open_ro() { 181 | Ok(v) => v, 182 | Err(err) => { 183 | error!("fuse: could not read inode {}: {}", ino, err); 184 | reply.error(libc::EIO); 185 | return; 186 | } 187 | }; 188 | 189 | let fh = self.next_read_handle; 190 | self.next_read_handle += 1; 191 | self.read_handles.insert(fh, reader); 192 | reply.opened(fh, flags); 193 | } 194 | 195 | fn read( 196 | &mut self, 197 | _req: &fuse::Request, 198 | ino: u64, 199 | fh: u64, 200 | offset: i64, 201 | size: u32, 202 | reply: fuse::ReplyData, 203 | ) { 204 | trace!( 205 | "fuse read: ino={}, fh={}, offset={}, size={}", 206 | ino, 207 | fh, 208 | offset, 209 | size 210 | ); 211 | 212 | let reader = match self.read_handles.get_mut(&fh) { 213 | Some(e) => e, 214 | None => { 215 | error!("fuse: no such open read handle, {}, inode {}", fh, ino); 216 | reply.error(libc::EBADF); 217 | return; 218 | } 219 | }; 220 | 221 | if let Err(err) = reader.seek(io::SeekFrom::Start(offset as u64)) { 222 | error!("fuse: {}", err); 223 | reply.error(libc::EIO); 224 | return; 225 | } 226 | trace!("seek to {} ok", offset); 227 | let mut buf = vec![0; size as usize]; 228 | let nread = match reader.read(&mut buf[..]) { 229 | Ok(v) => v, 230 | Err(err) => { 231 | error!("fuse: {}", err); 232 | reply.error(libc::EIO); 233 | return; 234 | } 235 | }; 236 | trace!("read {} bytes ok", nread); 237 | reply.data(&buf[..nread]); 238 | } 239 | 240 | fn release( 241 | &mut self, 242 | _req: &fuse::Request, 243 | ino: u64, 244 | fh: u64, 245 | flags: u32, 246 | lock_owner: u64, 247 | flush: bool, 248 | reply: fuse::ReplyEmpty, 249 | ) { 250 | trace!( 251 | "fuse release: {}, {}, {}, {}, {}", 252 | ino, 253 | fh, 254 | flags, 255 | lock_owner, 256 | flush 257 | ); 258 | 259 | self.read_handles.remove(&fh); 260 | reply.ok(); 261 | } 262 | 263 | fn opendir( 264 | &mut self, 265 | _req: &fuse::Request, 266 | parent_ino: u64, 267 | flags: u32, 268 | reply: fuse::ReplyOpen, 269 | ) { 270 | trace!("fuse opendir: {}, {}", parent_ino, flags); 271 | 272 | let children = { 273 | let node = match self.nodes.get(&parent_ino) { 274 | Some(v) => v, 275 | None => { 276 | error!("fuse: no entry for inode {}", parent_ino); 277 | reply.error(libc::ENOENT); 278 | return; 279 | } 280 | }; 281 | let dir = node 282 | .directory() 283 | .expect("can not call lookup on a non-directory"); 284 | match dir.files() { 285 | Ok(v) => v, 286 | Err(err) => { 287 | error!( 288 | "fuse: could not get children for inode {}: {}", 289 | parent_ino, err 290 | ); 291 | reply.error(libc::EIO); 292 | return; 293 | } 294 | } 295 | }; 296 | let entries = children 297 | .into_iter() 298 | .map(|(name, entry)| { 299 | let ino = inode_for_child(parent_ino, &name); 300 | (name, entry, ino) 301 | }) 302 | .collect(); 303 | 304 | let fh = self.next_readdir_handle; 305 | self.next_readdir_handle += 1; 306 | self.readdir_handles.insert(fh, entries); 307 | reply.opened(fh, flags); 308 | } 309 | 310 | fn readdir( 311 | &mut self, 312 | _req: &fuse::Request, 313 | parent_ino: u64, 314 | fh: u64, 315 | offset: i64, 316 | mut reply: fuse::ReplyDirectory, 317 | ) { 318 | trace!("fuse readdir: {}, {}, {}", parent_ino, fh, offset); 319 | 320 | let entries = match self.readdir_handles.get(&fh) { 321 | Some(e) => e, 322 | None => { 323 | error!( 324 | "fuse: no open readdir handle for handle {}, inode {}", 325 | fh, parent_ino 326 | ); 327 | reply.error(libc::EBADF); 328 | return; 329 | } 330 | }; 331 | 332 | let iter = entries.iter().skip(offset as usize).enumerate(); 333 | for (i, (name, node, ino)) in iter { 334 | let typ = filetype_for_node(&node); 335 | trace!("fuse readdir node: {} {:?}, {}", ino, typ, name); 336 | if reply.add(*ino, offset + i as i64 + 1, typ, name) { 337 | break; 338 | } 339 | } 340 | reply.ok(); 341 | } 342 | 343 | fn releasedir( 344 | &mut self, 345 | _req: &fuse::Request, 346 | parent_ino: u64, 347 | fh: u64, 348 | flags: u32, 349 | reply: fuse::ReplyEmpty, 350 | ) { 351 | trace!("fuse releasedir: {}, {}, {}", parent_ino, fh, flags); 352 | 353 | self.readdir_handles.remove(&fh); 354 | reply.ok(); 355 | } 356 | 357 | fn access(&mut self, _req: &fuse::Request, ino: u64, mask: u32, reply: fuse::ReplyEmpty) { 358 | trace!("fuse access: {}, {}", ino, mask); 359 | reply.ok(); 360 | } 361 | 362 | // fn getlk( 363 | // &mut self, 364 | // _req: &fuse::Request, 365 | // _ino: u64, 366 | // _fh: u64, 367 | // _lock_owner: u64, 368 | // _start: u64, 369 | // _end: u64, 370 | // _typ: u32, 371 | // _pid: u32, 372 | // _reply: fuse::ReplyLock, 373 | // ) { 374 | // unimplemented!(); 375 | // } 376 | // fn setlk( 377 | // &mut self, 378 | // _req: &fuse::Request, 379 | // _ino: u64, 380 | // _fh: u64, 381 | // _lock_owner: u64, 382 | // _start: u64, 383 | // _end: u64, 384 | // _typ: u32, 385 | // _pid: u32, 386 | // _sleep: bool, 387 | // _reply: fuse::ReplyEmpty, 388 | // ) { 389 | // unimplemented!(); 390 | // } 391 | // fn statfs(&mut self, _req: &fuse::Request, ino: u64, _reply: fuse::ReplyStatfs) { 392 | // } 393 | // fn getxattr( 394 | // &mut self, 395 | // _req: &fuse::Request, 396 | // _ino: u64, 397 | // _os_name: &ffi::OsStr, 398 | // _size: u32, 399 | // reply: fuse::ReplyXattr, 400 | // ) { 401 | // unimplemented!(); 402 | // } 403 | // 404 | // fn listxattr(&mut self, _req: &fuse::Request, _ino: u64, _size: u32, _reply: fuse::ReplyXattr) { 405 | // unimplemented!(); 406 | // } 407 | // fn forget(&mut self, _req: &Request, _ino: u64, _nlookup: u64) { ... } 408 | // fn setattr( 409 | // &mut self, 410 | // _req: &Request, 411 | // _ino: u64, 412 | // _mode: Option, 413 | // _uid: Option, 414 | // _gid: Option, 415 | // _size: Option, 416 | // _atime: Option, 417 | // _mtime: Option, 418 | // _fh: Option, 419 | // _crtime: Option, 420 | // _chgtime: Option, 421 | // _bkuptime: Option, 422 | // _flags: Option, 423 | // reply: ReplyAttr 424 | // ) { ... } 425 | // fn mknod( 426 | // &mut self, 427 | // _req: &Request, 428 | // _parent: u64, 429 | // _name: &OsStr, 430 | // _mode: u32, 431 | // _rdev: u32, 432 | // reply: ReplyEntry 433 | // ) { ... } 434 | // fn mkdir( 435 | // &mut self, 436 | // _req: &Request, 437 | // _parent: u64, 438 | // _name: &OsStr, 439 | // _mode: u32, 440 | // reply: ReplyEntry 441 | // ) { ... } 442 | // fn unlink( 443 | // &mut self, 444 | // _req: &Request, 445 | // _parent: u64, 446 | // _name: &OsStr, 447 | // reply: ReplyEmpty 448 | // ) { ... } 449 | // fn rmdir( 450 | // &mut self, 451 | // _req: &Request, 452 | // _parent: u64, 453 | // _name: &OsStr, 454 | // reply: ReplyEmpty 455 | // ) { ... } 456 | // fn symlink( 457 | // &mut self, 458 | // _req: &Request, 459 | // _parent: u64, 460 | // _name: &OsStr, 461 | // _link: &Path, 462 | // reply: ReplyEntry 463 | // ) { ... } 464 | // fn rename( 465 | // &mut self, 466 | // _req: &Request, 467 | // _parent: u64, 468 | // _name: &OsStr, 469 | // _newparent: u64, 470 | // _newname: &OsStr, 471 | // reply: ReplyEmpty 472 | // ) { ... } 473 | // fn link( 474 | // &mut self, 475 | // _req: &Request, 476 | // _ino: u64, 477 | // _newparent: u64, 478 | // _newname: &OsStr, 479 | // reply: ReplyEntry 480 | // ) { ... } 481 | // fn write( 482 | // &mut self, 483 | // _req: &Request, 484 | // _ino: u64, 485 | // _fh: u64, 486 | // _offset: i64, 487 | // _data: &[u8], 488 | // _flags: u32, 489 | // reply: ReplyWrite 490 | // ) { ... } 491 | // fn flush( 492 | // &mut self, 493 | // _req: &Request, 494 | // _ino: u64, 495 | // _fh: u64, 496 | // _lock_owner: u64, 497 | // reply: ReplyEmpty 498 | // ) { ... } 499 | // fn fsync( 500 | // &mut self, 501 | // _req: &Request, 502 | // _ino: u64, 503 | // _fh: u64, 504 | // _datasync: bool, 505 | // reply: ReplyEmpty 506 | // ) { ... } 507 | // fn fsyncdir( 508 | // &mut self, 509 | // _req: &Request, 510 | // _ino: u64, 511 | // _fh: u64, 512 | // _datasync: bool, 513 | // reply: ReplyEmpty 514 | // ) { ... } 515 | // fn setxattr( 516 | // &mut self, 517 | // _req: &Request, 518 | // _ino: u64, 519 | // _name: &OsStr, 520 | // _value: &[u8], 521 | // _flags: u32, 522 | // _position: u32, 523 | // reply: ReplyEmpty 524 | // ) { ... } 525 | // fn removexattr( 526 | // &mut self, 527 | // _req: &Request, 528 | // _ino: u64, 529 | // _name: &OsStr, 530 | // reply: ReplyEmpty 531 | // ) { ... } 532 | // fn create( 533 | // &mut self, 534 | // _req: &Request, 535 | // _parent: u64, 536 | // _name: &OsStr, 537 | // _mode: u32, 538 | // _flags: u32, 539 | // reply: ReplyCreate 540 | // ) { } 541 | // fn bmap( 542 | // &mut self, 543 | // _req: &Request, 544 | // _ino: u64, 545 | // _blocksize: u32, 546 | // _idx: u64, 547 | // reply: ReplyBmap 548 | // ) { } 549 | } 550 | 551 | fn inode_for_child(parent_ino: u64, name: &str) -> u64 { 552 | let mut s = DefaultHasher::new(); 553 | parent_ino.hash(&mut s); 554 | name.hash(&mut s); 555 | s.finish() 556 | } 557 | 558 | fn filetype_for_node(node: &Node) -> fuse::FileType { 559 | match node { 560 | Node::File(_) => fuse::FileType::RegularFile, 561 | Node::Directory(_) => fuse::FileType::Directory, 562 | Node::Symlink(_) => fuse::FileType::Symlink, 563 | } 564 | } 565 | 566 | fn timespec_from_datetime(t: &DateTime) -> time::Timespec { 567 | time::Timespec::new(t.timestamp(), t.timestamp_subsec_nanos() as i32) 568 | } 569 | 570 | fn attrs_for_file( 571 | node: &Node, 572 | ino: u64, 573 | uid: u32, 574 | gid: u32, 575 | ) -> Result { 576 | const BLOCK_SIZE: u64 = 1024; 577 | 578 | let meta = node.metadata()?; 579 | let size = match node { 580 | Node::File(f) => f.size()?, 581 | _ => 0, 582 | }; 583 | Ok(fuse::FileAttr { 584 | ino, 585 | size, 586 | blocks: size / BLOCK_SIZE, 587 | atime: timespec_from_datetime(&meta.mtime), 588 | mtime: timespec_from_datetime(&meta.mtime), 589 | ctime: timespec_from_datetime(&meta.ctime), 590 | crtime: timespec_from_datetime(&meta.ctime), 591 | kind: filetype_for_node(&node), 592 | perm: meta.perm, 593 | nlink: 1, 594 | uid, 595 | gid, 596 | rdev: 0, 597 | flags: 0, 598 | }) 599 | } 600 | -------------------------------------------------------------------------------- /src/filesystem/node.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use chrono::{DateTime, Utc}; 3 | use std::error; 4 | use std::iter::Iterator; 5 | use std::path::PathBuf; 6 | 7 | pub trait Error: error::Error { 8 | fn not_found() -> Self; 9 | fn errno(&self) -> i32; 10 | } 11 | 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct Metadata { 14 | pub mtime: DateTime, 15 | pub ctime: DateTime, 16 | pub perm: u16, 17 | } 18 | 19 | pub trait Meta { 20 | type Error: Error; 21 | fn metadata(&self) -> Result; 22 | } 23 | 24 | pub trait File: Meta { 25 | type Reader: io::Read + io::Seek; 26 | fn open_ro(&self) -> Result; 27 | fn size(&self) -> Result; 28 | } 29 | 30 | pub trait Directory: Meta { 31 | fn files(&self) -> Result)>, Self::Error>; 32 | 33 | fn file_by_name(&self, name: &str) -> Result, Self::Error> { 34 | self.files()? 35 | .into_iter() 36 | .find(|(n, _)| n == name) 37 | .map(|(_, entry)| entry) 38 | .ok_or_else(Self::Error::not_found) 39 | } 40 | } 41 | 42 | pub trait Symlink: Meta { 43 | fn read_link(&self) -> Result; 44 | } 45 | 46 | pub trait NodeType: Sized { 47 | type Error: Error; 48 | type File: File; 49 | type Directory: Directory; 50 | type Symlink: Symlink; 51 | 52 | fn root(&self) -> Self::Directory; 53 | } 54 | 55 | #[derive(Clone)] 56 | pub enum Node { 57 | File(T::File), 58 | Directory(T::Directory), 59 | Symlink(T::Symlink), 60 | } 61 | 62 | impl Node { 63 | pub fn file(&self) -> Option<&T::File> { 64 | match self { 65 | Node::File(ref f) => Some(f), 66 | _ => None, 67 | } 68 | } 69 | 70 | pub fn directory(&self) -> Option<&T::Directory> { 71 | match self { 72 | Node::Directory(ref f) => Some(f), 73 | _ => None, 74 | } 75 | } 76 | 77 | pub fn symlink(&self) -> Option<&T::Symlink> { 78 | match self { 79 | Node::Symlink(ref f) => Some(f), 80 | _ => None, 81 | } 82 | } 83 | } 84 | 85 | impl Meta for Node { 86 | type Error = T::Error; 87 | fn metadata(&self) -> Result { 88 | match self { 89 | Node::File(f) => f.metadata(), 90 | Node::Directory(f) => f.metadata(), 91 | Node::Symlink(f) => f.metadata(), 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/filesystem/nodecache.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::cell::RefCell; 3 | use std::collections::{HashMap, HashSet}; 4 | 5 | #[derive(Clone)] 6 | pub struct CacheRoot 7 | where 8 | N: NodeType + Clone, 9 | N::File: Clone, 10 | N::Directory: Clone, 11 | N::Symlink: Clone, 12 | { 13 | root: DirCache, 14 | } 15 | 16 | impl CacheRoot 17 | where 18 | N: NodeType + Clone, 19 | N::File: Clone, 20 | N::Directory: Clone, 21 | N::Symlink: Clone, 22 | { 23 | pub fn new(inner: &N) -> Self { 24 | CacheRoot { 25 | root: DirCache::new(inner.root()), 26 | } 27 | } 28 | } 29 | 30 | impl NodeType for CacheRoot 31 | where 32 | N: NodeType + Clone, 33 | N::File: Clone, 34 | N::Directory: Clone, 35 | N::Symlink: Clone, 36 | { 37 | type Error = N::Error; 38 | type File = N::File; 39 | type Directory = DirCache; 40 | type Symlink = N::Symlink; 41 | 42 | fn root(&self) -> Self::Directory { 43 | self.root.clone() 44 | } 45 | } 46 | 47 | #[derive(Clone)] 48 | pub struct DirCache 49 | where 50 | N: NodeType + Clone, 51 | N::File: Clone, 52 | N::Directory: Clone, 53 | N::Symlink: Clone, 54 | { 55 | inner: N::Directory, 56 | cached_files: RefCell>)>>>, 57 | hidden_cached_files: RefCell>>>, 58 | non_files: RefCell>, 59 | } 60 | 61 | impl DirCache 62 | where 63 | N: NodeType + Clone, 64 | N::File: Clone, 65 | N::Directory: Clone, 66 | N::Symlink: Clone, 67 | { 68 | pub fn new(inner: N::Directory) -> Self { 69 | DirCache { 70 | inner, 71 | cached_files: RefCell::new(None), 72 | hidden_cached_files: RefCell::new(HashMap::new()), 73 | non_files: RefCell::new(HashSet::new()), 74 | } 75 | } 76 | } 77 | 78 | impl Meta for DirCache 79 | where 80 | N: NodeType + Clone, 81 | N::File: Clone, 82 | N::Directory: Clone, 83 | N::Symlink: Clone, 84 | { 85 | type Error = N::Error; 86 | fn metadata(&self) -> Result { 87 | self.inner.metadata() 88 | } 89 | } 90 | 91 | impl Directory> for DirCache 92 | where 93 | N: NodeType + Clone, 94 | N::File: Clone, 95 | N::Directory: Clone, 96 | N::Symlink: Clone, 97 | { 98 | fn files(&self) -> Result>)>, Self::Error> { 99 | let mut cached = self.cached_files.borrow_mut(); 100 | if cached.is_some() { 101 | return Ok(cached.as_ref().unwrap().to_vec()); 102 | } 103 | let files: Vec<_> = self 104 | .inner 105 | .files()? 106 | .into_iter() 107 | .map(|(name, node)| (name, map_node(node))) 108 | .collect(); 109 | *cached = Some(files.clone()); 110 | Ok(files) 111 | } 112 | 113 | fn file_by_name(&self, name: &str) -> Result>, Self::Error> { 114 | if self.non_files.borrow().contains(name) { 115 | return Err(Self::Error::not_found()); 116 | } 117 | 118 | if let Some(node) = self.hidden_cached_files.borrow().get(name) { 119 | return Ok(node.clone()); 120 | } 121 | 122 | let cached = self.cached_files.borrow_mut(); 123 | if cached.is_some() { 124 | let maybe_node = cached 125 | .as_ref() 126 | .unwrap() 127 | .iter() 128 | .find(|(n, _)| n == name) 129 | .map(|(_, entry)| entry); 130 | if let Some(node) = maybe_node { 131 | return Ok(node.clone()); 132 | } 133 | } 134 | 135 | match self.inner.file_by_name(name) { 136 | Ok(node) => { 137 | let node = map_node(node); 138 | self.hidden_cached_files 139 | .borrow_mut() 140 | .insert(name.to_string(), node.clone()); 141 | Ok(node) 142 | } 143 | Err(err) => { 144 | if err.errno() == libc::ENOENT { 145 | self.non_files.borrow_mut().insert(name.to_string()); 146 | } 147 | Err(err) 148 | } 149 | } 150 | } 151 | } 152 | 153 | fn map_node(node: Node) -> Node> 154 | where 155 | N: NodeType + Clone, 156 | N::File: Clone, 157 | N::Directory: Clone, 158 | N::Symlink: Clone, 159 | { 160 | match node { 161 | Node::File(f) => Node::File(f), 162 | Node::Directory(f) => Node::Directory(DirCache::new(f)), 163 | Node::Symlink(f) => Node::Symlink(f), 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/id3tag.rs: -------------------------------------------------------------------------------- 1 | use crate::soundcloud; 2 | use chrono::Datelike; 3 | use id3; 4 | use log::*; 5 | use std::io; 6 | 7 | pub fn tag_for_track( 8 | track: &soundcloud::Track, 9 | enable_artwork: bool, 10 | parse_strings: bool, 11 | ) -> Result { 12 | let mut tag = id3::Tag::new(); 13 | 14 | if let Some(i) = track.title.find(" - ").filter(|_| parse_strings) { 15 | tag.set_title(&track.title[..i]); 16 | tag.set_artist(&track.title[i + 3..]); 17 | } else { 18 | tag.set_artist(track.user.username.as_str()); 19 | tag.set_title(track.title.as_str()); 20 | } 21 | 22 | tag.set_duration(track.duration_ms as u32); 23 | tag.set_text("TCOP", track.license.as_str()); 24 | tag.add_frame(id3::Frame::with_content( 25 | "WOAF", 26 | id3::Content::Link(track.permalink_url.to_string()), 27 | )); 28 | tag.add_frame(id3::Frame::with_content( 29 | "WOAR", 30 | id3::Content::Link(track.user.permalink_url.to_string()), 31 | )); 32 | tag.set_year( 33 | track 34 | .release_year 35 | .unwrap_or_else(|| track.created_at.date().year()), 36 | ); 37 | tag.set_text( 38 | "TDAT", 39 | format!( 40 | "{:02}{:02}", 41 | track.created_at.date().day(), 42 | track.created_at.date().month(), 43 | ), 44 | ); 45 | if let Some(ref descrtiption) = track.description { 46 | tag.add_comment(id3::frame::Comment { 47 | lang: "eng".to_string(), 48 | description: "Description".to_string(), 49 | text: descrtiption.clone(), 50 | }); 51 | } 52 | if let Some(year) = track.release_year { 53 | tag.set_text("TORY", format!("{}", year)); 54 | } 55 | if let Some(ref genre) = track.genre { 56 | tag.set_genre(genre.as_str()); 57 | } 58 | if let Some(bpm) = track.bpm { 59 | tag.set_text("TBPM", format!("{}", bpm.round())); 60 | } 61 | if let Some(ref label) = track.label_name { 62 | tag.set_text("TPUB", label.as_str()); 63 | } 64 | if let Some(ref isrc) = track.isrc { 65 | tag.set_text("TSRC", isrc.as_str()); 66 | } 67 | 68 | if enable_artwork { 69 | match track.artwork() { 70 | Err(soundcloud::Error::ArtworkNotAvailable) => (), 71 | Err(err) => error!("{}", err), 72 | Ok((data, mime_type)) => tag.add_picture(id3::frame::Picture { 73 | mime_type, 74 | picture_type: id3::frame::PictureType::CoverFront, 75 | description: "Artwork".to_string(), 76 | data, 77 | }), 78 | } 79 | } 80 | 81 | let mut id3_tag_buf = Vec::new(); 82 | tag.write_to(&mut id3_tag_buf, id3::Version::Id3v24) 83 | .unwrap(); 84 | Ok(io::Cursor::new(id3_tag_buf)) 85 | } 86 | -------------------------------------------------------------------------------- /src/ioutil/concat.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::io; 3 | use std::ops::Range; 4 | 5 | /// Concat is a collection of files that are concatenated after one another as if they were a 6 | /// single file. 7 | /// 8 | /// It is assumed that the underlying files do not change in size. 9 | pub struct Concat 10 | where 11 | T: io::Read, 12 | { 13 | files: Vec, 14 | chunk_index: usize, 15 | offset: u64, 16 | 17 | ranges: Vec>, 18 | } 19 | 20 | impl Concat 21 | where 22 | T: io::Read, 23 | { 24 | pub fn new(files: Vec) -> Self { 25 | Concat { 26 | files, 27 | ranges: Vec::new(), 28 | chunk_index: 0, 29 | offset: 0, 30 | } 31 | } 32 | } 33 | 34 | impl Concat 35 | where 36 | T: io::Read + io::Seek, 37 | { 38 | /// index_up_to ensures that there is a range in `self.ranges` that includes the specified 39 | /// offset unless there are no more files to index. 40 | fn index_up_to(&mut self, new_offset: u64) -> io::Result<()> { 41 | loop { 42 | let current_end = self.ranges.last().map(|r| r.end).unwrap_or(0); 43 | if (new_offset as u64) < current_end { 44 | break Ok(()); 45 | } 46 | 47 | let file = match self.files.get_mut(self.ranges.len()) { 48 | Some(v) => v, 49 | None => break Ok(()), 50 | }; 51 | let size = file.seek(io::SeekFrom::End(0))?; 52 | let range = current_end..(current_end + size); 53 | self.ranges.push(range); 54 | } 55 | } 56 | } 57 | 58 | impl io::Read for Concat 59 | where 60 | T: io::Read + io::Seek, 61 | { 62 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 63 | let num_files = self.files.len(); 64 | let mut total_nread = 0; 65 | let mut next_chunk = false; 66 | while total_nread < buf.len() && self.chunk_index < num_files { 67 | let file = &mut self.files[self.chunk_index]; 68 | if next_chunk { 69 | // If we transition into the next chunk, ensure that it is set to the beginning. 70 | file.seek(io::SeekFrom::Start(0))?; 71 | next_chunk = false; 72 | } 73 | 74 | let nread = file.read(&mut buf[total_nread..])?; 75 | if nread == 0 { 76 | self.chunk_index += 1; 77 | next_chunk = true; 78 | if self.chunk_index >= num_files { 79 | break; 80 | } 81 | continue; 82 | } 83 | total_nread += nread; 84 | self.offset += nread as u64; 85 | } 86 | 87 | Ok(total_nread) 88 | } 89 | } 90 | 91 | impl io::Seek for Concat 92 | where 93 | T: io::Read + io::Seek, 94 | { 95 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 96 | let new_offset: i64 = match pos { 97 | io::SeekFrom::Start(offset) => { 98 | self.index_up_to(offset)?; 99 | offset as i64 100 | } 101 | io::SeekFrom::Current(offset) => { 102 | let new_offset = self.offset as i64 + offset; 103 | if new_offset < 0 { 104 | return Err(io::Error::new( 105 | io::ErrorKind::Other, 106 | format!( 107 | "ioutil::Concat: seek position {:?} resolves to {}", 108 | pos, new_offset 109 | ), 110 | )); 111 | } 112 | self.index_up_to(new_offset as u64)?; 113 | new_offset 114 | } 115 | io::SeekFrom::End(offset) => { 116 | // Before we can know where the end is, we have to index all files. We do this by 117 | // just trying to index up the closest we can get to infinity with a 64 bit int. 118 | self.index_up_to(std::u64::MAX)?; 119 | let length = self.ranges.last().unwrap().end as i64; 120 | let new_offset = length + offset; 121 | if new_offset < 0 { 122 | return Err(io::Error::new( 123 | io::ErrorKind::Other, 124 | format!( 125 | "ioutil::Concat: seek position {:?} resolves to {}", 126 | pos, new_offset 127 | ), 128 | )); 129 | } 130 | new_offset 131 | } 132 | }; 133 | 134 | self.offset = new_offset as u64; 135 | let new_chunk_index = self 136 | .ranges 137 | .binary_search_by(|range| { 138 | if self.offset < range.start { 139 | Ordering::Greater 140 | } else if self.offset >= range.end { 141 | Ordering::Less 142 | } else { 143 | Ordering::Equal 144 | } 145 | }) 146 | .ok() 147 | .or_else(|| self.ranges.len().checked_sub(1)); 148 | if let Some(i) = new_chunk_index { 149 | self.chunk_index = i; 150 | let file = &mut self.files[self.chunk_index]; 151 | let range = &mut self.ranges[self.chunk_index]; 152 | file.seek(io::SeekFrom::Start(new_offset as u64 - range.start))?; 153 | } 154 | Ok(self.offset) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::super::OpRecorder; 161 | use super::*; 162 | use std::io::{Read, Seek}; 163 | 164 | #[test] 165 | fn read_single_file() { 166 | let expect = vec![1, 2, 3, 4, 5, 6, 7, 8]; 167 | let mut concat = Concat::new(vec![io::Cursor::new(expect.clone())]); 168 | 169 | let mut buf = vec![0; expect.len()]; 170 | let nread = concat.read(&mut buf).unwrap(); 171 | assert_eq!(nread, expect.len()); 172 | assert_eq!(buf, expect); 173 | 174 | let nread = concat.read(&mut buf).unwrap(); 175 | assert_eq!(nread, 0); 176 | } 177 | 178 | #[test] 179 | fn read_single_file_multi() { 180 | let expect = vec![1, 2, 3, 4, 5, 6, 7, 8]; 181 | let mut concat = Concat::new(vec![io::Cursor::new(expect.clone())]); 182 | 183 | let mut buf = vec![0; 4]; 184 | let nread = concat.read(&mut buf).unwrap(); 185 | assert_eq!(nread, 4); 186 | assert_eq!(buf, vec![1, 2, 3, 4]); 187 | 188 | let nread = concat.read(&mut buf).unwrap(); 189 | assert_eq!(nread, 4); 190 | assert_eq!(buf, vec![5, 6, 7, 8]); 191 | 192 | let nread = concat.read(&mut buf).unwrap(); 193 | assert_eq!(nread, 0); 194 | } 195 | 196 | #[test] 197 | fn read_multiple_files() { 198 | let a = vec![1, 2, 3, 4]; 199 | let b = vec![5, 6, 7, 8]; 200 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 201 | 202 | let expect = vec![1, 2, 3, 4, 5, 6, 7, 8]; 203 | let mut buf = vec![0; expect.len()]; 204 | let nread = concat.read(&mut buf).unwrap(); 205 | assert_eq!(expect.len(), nread); 206 | assert_eq!(expect, buf); 207 | } 208 | 209 | #[test] 210 | fn read_multiple_files_multiread() { 211 | let a = vec![1, 2, 3, 4]; 212 | let b = vec![5, 6, 7, 8]; 213 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 214 | 215 | let mut buf = vec![0; 6]; 216 | let nread = concat.read(&mut buf).unwrap(); 217 | assert_eq!(nread, 6); 218 | assert_eq!(buf, vec![1, 2, 3, 4, 5, 6]); 219 | 220 | let mut buf = vec![0; 4]; 221 | let nread = concat.read(&mut buf).unwrap(); 222 | assert_eq!(nread, 2); 223 | assert_eq!(buf, vec![7, 8, 0, 0]); 224 | 225 | let nread = concat.read(&mut buf).unwrap(); 226 | assert_eq!(nread, 0); 227 | } 228 | 229 | #[test] 230 | fn seek_single_file() { 231 | let expect = vec![1, 2, 3, 4, 5, 6, 7, 8]; 232 | let mut concat = Concat::new(vec![io::Cursor::new(expect.clone())]); 233 | 234 | concat.seek(io::SeekFrom::Start(4)).unwrap(); 235 | 236 | let mut buf = vec![0; 4]; 237 | let nread = concat.read(&mut buf).unwrap(); 238 | assert_eq!(nread, 4); 239 | assert_eq!(buf, vec![5, 6, 7, 8]); 240 | } 241 | 242 | #[test] 243 | fn seek_multiple_files_eof() { 244 | let a = vec![1, 2, 3, 4]; 245 | let b = vec![5, 6, 7, 8]; 246 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 247 | 248 | let abs_pos = concat.seek(io::SeekFrom::End(0)).unwrap(); 249 | assert_eq!(abs_pos, 8); 250 | 251 | let mut buf = vec![0; 4]; 252 | let nread = concat.read(&mut buf).unwrap(); 253 | assert_eq!(nread, 0); 254 | } 255 | 256 | #[test] 257 | fn seek_multiple_files_a() { 258 | let a = vec![1, 2, 3, 4]; 259 | let b = vec![5, 6, 7, 8]; 260 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 261 | 262 | concat.seek(io::SeekFrom::Start(2)).unwrap(); 263 | 264 | let mut buf = vec![0; 6]; 265 | let nread = concat.read(&mut buf).unwrap(); 266 | assert_eq!(nread, 6); 267 | assert_eq!(buf, vec![3, 4, 5, 6, 7, 8]); 268 | } 269 | 270 | #[test] 271 | fn seek_multiple_files_b() { 272 | let a = vec![1, 2, 3, 4]; 273 | let b = vec![5, 6, 7, 8]; 274 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 275 | 276 | concat.seek(io::SeekFrom::Start(6)).unwrap(); 277 | 278 | let mut buf = vec![0; 2]; 279 | let nread = concat.read(&mut buf).unwrap(); 280 | assert_eq!(nread, 2); 281 | assert_eq!(buf, vec![7, 8]); 282 | } 283 | 284 | #[test] 285 | fn seek_after_read() { 286 | let a = vec![1, 2, 3, 4]; 287 | let b = vec![5, 6, 7, 8]; 288 | let mut concat = Concat::new(vec![io::Cursor::new(a.clone()), io::Cursor::new(b.clone())]); 289 | 290 | let mut buf = vec![0; 2]; 291 | let nread = concat.read(&mut buf).unwrap(); 292 | assert_eq!(nread, 2); 293 | assert_eq!(buf, vec![1, 2]); 294 | 295 | concat.seek(io::SeekFrom::Start(6)).unwrap(); 296 | 297 | let mut buf = vec![0; 2]; 298 | let nread = concat.read(&mut buf).unwrap(); 299 | assert_eq!(nread, 2); 300 | assert_eq!(buf, vec![7, 8]); 301 | 302 | concat.seek(io::SeekFrom::Start(2)).unwrap(); 303 | 304 | let mut buf = vec![0; 2]; 305 | let nread = concat.read(&mut buf).unwrap(); 306 | assert_eq!(nread, 2); 307 | assert_eq!(buf, vec![3, 4]); 308 | } 309 | 310 | #[test] 311 | fn seek_lazy_ranges() { 312 | let mut concat = Concat::new(vec![ 313 | OpRecorder::new(io::Cursor::new(vec![0; 4])), 314 | OpRecorder::new(io::Cursor::new(vec![0; 4])), 315 | ]); 316 | 317 | let mut buf = vec![0; 4]; 318 | concat.read(&mut buf).unwrap(); 319 | concat.seek(io::SeekFrom::Start(3)).unwrap(); 320 | 321 | println!("{:?}", concat.files[1].ops()); 322 | assert_eq!(concat.files[1].ops().len(), 0); 323 | } 324 | 325 | #[test] 326 | fn seek_beyond_length() { 327 | let mut concat = Concat::new(vec![ 328 | io::Cursor::new(vec![0; 4]), 329 | io::Cursor::new(vec![0; 4]), 330 | ]); 331 | 332 | concat.seek(io::SeekFrom::End(8)).unwrap(); 333 | 334 | let mut buf = vec![0; 4]; 335 | let nread = concat.read(&mut buf).unwrap(); 336 | assert_eq!(nread, 0); 337 | } 338 | 339 | #[test] 340 | fn empty_read() { 341 | let mut concat = Concat::new(Vec::>::new()); 342 | 343 | let mut buf = vec![0; 4]; 344 | let nread = concat.read(&mut buf).unwrap(); 345 | assert_eq!(nread, 0); 346 | } 347 | 348 | #[test] 349 | fn empty_seek() { 350 | let mut concat = Concat::new(Vec::>::new()); 351 | 352 | concat.seek(io::SeekFrom::Start(4)).unwrap(); 353 | 354 | let mut buf = vec![0; 4]; 355 | let nread = concat.read(&mut buf).unwrap(); 356 | assert_eq!(nread, 0); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/ioutil/lazyopen.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::mem; 3 | 4 | enum State 5 | where 6 | O: FnOnce() -> io::Result, 7 | T: io::Read, 8 | { 9 | Unopened(O), 10 | Opened(T), 11 | Error(io::Error), 12 | Init, 13 | } 14 | 15 | pub struct LazyOpen 16 | where 17 | O: FnOnce() -> io::Result, 18 | T: io::Read, 19 | { 20 | state: State, 21 | size_hint: Option, 22 | size_hint_seek_dirty: Option, 23 | } 24 | 25 | impl LazyOpen 26 | where 27 | O: FnOnce() -> io::Result, 28 | T: io::Read, 29 | { 30 | #[allow(unused)] 31 | pub fn new(open_fn: O) -> Self { 32 | LazyOpen { 33 | state: State::Unopened(open_fn), 34 | size_hint: None, 35 | size_hint_seek_dirty: None, 36 | } 37 | } 38 | 39 | pub fn with_size_hint(size_hint: u64, open_fn: O) -> Self { 40 | LazyOpen { 41 | state: State::Unopened(open_fn), 42 | size_hint: Some(size_hint), 43 | size_hint_seek_dirty: None, 44 | } 45 | } 46 | 47 | fn file_mut(&mut self) -> io::Result<&mut T> { 48 | match self.state { 49 | State::Opened(ref mut file) => { 50 | return Ok(file); 51 | } 52 | State::Error(ref err) => { 53 | return Err(io::Error::new(err.kind(), format!("{}", err))); 54 | } 55 | State::Unopened(_) => (), 56 | State::Init => unreachable!(), 57 | }; 58 | 59 | let mut init = State::Init; 60 | mem::swap(&mut self.state, &mut init); 61 | let open_fn = match init { 62 | State::Unopened(v) => v, 63 | _ => unreachable!(), 64 | }; 65 | 66 | self.state = match open_fn() { 67 | Ok(file) => State::Opened(file), 68 | Err(err) => State::Error(err), 69 | }; 70 | self.file_mut() 71 | } 72 | } 73 | 74 | impl io::Read for LazyOpen 75 | where 76 | O: FnOnce() -> io::Result, 77 | T: io::Read + io::Seek, 78 | { 79 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 80 | if let Some(pos) = self.size_hint_seek_dirty.take() { 81 | let file = self.file_mut()?; 82 | file.seek(pos)?; 83 | } 84 | let file = self.file_mut()?; 85 | file.read(buf) 86 | } 87 | } 88 | 89 | impl io::Seek for LazyOpen 90 | where 91 | O: FnOnce() -> io::Result, 92 | T: io::Read + io::Seek, 93 | { 94 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 95 | if let Some(s) = self.size_hint { 96 | if pos == io::SeekFrom::End(0) { 97 | self.size_hint_seek_dirty = Some(pos); 98 | return Ok(s); 99 | } 100 | } 101 | let file = self.file_mut()?; 102 | file.seek(pos) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | use std::io::{Read, Seek}; 110 | 111 | #[test] 112 | fn open_read() { 113 | let data = vec![1, 2, 3, 4]; 114 | let mut file = LazyOpen::new(|| Ok(io::Cursor::new(&data))); 115 | 116 | let mut buf = vec![0; 4]; 117 | let nread = file.read(&mut buf).unwrap(); 118 | assert_eq!(nread, 4); 119 | assert_eq!(buf, data); 120 | } 121 | 122 | #[test] 123 | fn open_seek() { 124 | let data = vec![1, 2, 3, 4]; 125 | let mut file = LazyOpen::new(|| Ok(io::Cursor::new(data))); 126 | 127 | let new_pos = file.seek(io::SeekFrom::Start(2)).unwrap(); 128 | assert_eq!(new_pos, 2); 129 | 130 | let mut buf = vec![0; 2]; 131 | let nread = file.read(&mut buf).unwrap(); 132 | assert_eq!(nread, 2); 133 | assert_eq!(buf, vec![3, 4]); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ioutil/mod.rs: -------------------------------------------------------------------------------- 1 | mod concat; 2 | mod lazyopen; 3 | mod pattern; 4 | mod readseek; 5 | mod skip; 6 | 7 | #[allow(unused)] 8 | mod oprecorder; 9 | 10 | pub use self::concat::*; 11 | pub use self::lazyopen::*; 12 | pub use self::pattern::*; 13 | pub use self::readseek::*; 14 | pub use self::skip::*; 15 | 16 | #[doc(hidden)] 17 | pub use self::oprecorder::*; 18 | -------------------------------------------------------------------------------- /src/ioutil/oprecorder.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | pub enum Operation { 5 | Read { buflen: usize, nread: usize }, 6 | Seek { pos: io::SeekFrom, new_pos: u64 }, 7 | } 8 | 9 | pub struct OpRecorder { 10 | inner: T, 11 | ops: Vec, 12 | } 13 | 14 | impl OpRecorder { 15 | pub fn new(inner: T) -> OpRecorder { 16 | OpRecorder { 17 | inner, 18 | ops: Vec::new(), 19 | } 20 | } 21 | 22 | pub fn ops(&self) -> &[Operation] { 23 | &self.ops 24 | } 25 | } 26 | 27 | impl io::Read for OpRecorder 28 | where 29 | T: io::Read, 30 | { 31 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 32 | let nread = self.inner.read(buf)?; 33 | self.ops.push(Operation::Read { 34 | buflen: buf.len(), 35 | nread, 36 | }); 37 | Ok(nread) 38 | } 39 | } 40 | 41 | impl io::Seek for OpRecorder 42 | where 43 | T: io::Read + io::Seek, 44 | { 45 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 46 | let new_pos = self.inner.seek(pos)?; 47 | self.ops.push(Operation::Seek { pos, new_pos }); 48 | Ok(new_pos) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ioutil/pattern.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub struct Pattern> { 4 | pat: T, 5 | size: u64, 6 | offset: u64, 7 | } 8 | 9 | impl> Pattern { 10 | pub fn new(pat: T, size: u64) -> Self { 11 | Pattern { 12 | pat, 13 | size, 14 | offset: 0, 15 | } 16 | } 17 | } 18 | 19 | impl> io::Read for Pattern { 20 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 21 | let pat_len = self.pat.as_ref().len(); 22 | if pat_len == 0 { 23 | return Ok(0); 24 | } 25 | 26 | let buf_len = (self.size - self.offset).min(buf.len() as u64) as usize; 27 | let mut sub_buf = &mut buf[..buf_len]; 28 | 29 | while !sub_buf.is_empty() { 30 | let pat_start = (self.offset % pat_len as u64) as usize; 31 | let pat_end = (pat_start + sub_buf.len()).min(pat_len); 32 | let buf_end = sub_buf.len().min(pat_end - pat_start); 33 | 34 | sub_buf[..buf_end].copy_from_slice(&self.pat.as_ref()[pat_start..pat_end]); 35 | sub_buf = &mut sub_buf[buf_end..]; 36 | self.offset += buf_end as u64; 37 | } 38 | 39 | Ok(buf_len) 40 | } 41 | } 42 | 43 | impl> io::Seek for Pattern { 44 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 45 | let new_offset = match pos { 46 | io::SeekFrom::Start(offset) => offset as i64, 47 | io::SeekFrom::Current(offset) => self.offset as i64 + offset, 48 | io::SeekFrom::End(offset) => self.size as i64 + offset, 49 | }; 50 | if new_offset < 0 { 51 | return Err(io::Error::new( 52 | io::ErrorKind::Other, 53 | format!( 54 | "ioutil::Pattern: seek position {:?} resolves to {}", 55 | pos, new_offset 56 | ), 57 | )); 58 | } 59 | self.offset = new_offset as u64; 60 | Ok(self.offset) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | use std::io::Read; 68 | 69 | #[test] 70 | fn read_zero_sized() { 71 | let mut pat = Pattern::new([], 16); 72 | let mut buf = [0; 16]; 73 | let nread = pat.read(&mut buf[..]).unwrap(); 74 | assert_eq!(0, nread); 75 | } 76 | 77 | #[test] 78 | fn read_partial() { 79 | let mut pat = Pattern::new([1, 2, 3, 4, 5, 6, 7, 8], 16); 80 | let mut buf = [0; 4]; 81 | let nread = pat.read(&mut buf[..]).unwrap(); 82 | assert_eq!(nread, 4); 83 | assert_eq!(buf, [1, 2, 3, 4]); 84 | } 85 | 86 | #[test] 87 | fn read_once_exact() { 88 | let mut pat = Pattern::new([1, 2, 3, 4, 5, 6, 7, 8], 8); 89 | let mut buf = [0; 8]; 90 | let nread = pat.read(&mut buf[..]).unwrap(); 91 | assert_eq!(nread, 8); 92 | assert_eq!(buf, [1, 2, 3, 4, 5, 6, 7, 8]); 93 | } 94 | 95 | #[test] 96 | fn read_once_large_buf() { 97 | let mut pat = Pattern::new([1, 2, 3, 4, 5, 6, 7, 8], 8); 98 | let mut buf = [0; 16]; 99 | let nread = pat.read(&mut buf[..]).unwrap(); 100 | assert_eq!(nread, 8); 101 | assert_eq!(buf, [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]); 102 | } 103 | 104 | #[test] 105 | fn read_multi_exact() { 106 | let mut pat = Pattern::new([1, 2, 3, 4], 8); 107 | let mut buf = [0; 8]; 108 | let nread = pat.read(&mut buf[..]).unwrap(); 109 | assert_eq!(nread, 8); 110 | assert_eq!(buf, [1, 2, 3, 4, 1, 2, 3, 4]); 111 | } 112 | 113 | #[test] 114 | fn read_multi_partial() { 115 | let mut pat = Pattern::new([1, 2, 3, 4], 32); 116 | let mut buf = [0; 10]; 117 | let nread = pat.read(&mut buf[..]).unwrap(); 118 | assert_eq!(nread, 10); 119 | assert_eq!(buf, [1, 2, 3, 4, 1, 2, 3, 4, 1, 2]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ioutil/readseek.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub trait ReadSeek: io::Read + io::Seek {} 4 | 5 | impl ReadSeek for T where T: io::Read + io::Seek {} 6 | -------------------------------------------------------------------------------- /src/ioutil/skip.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub struct Skip 4 | where 5 | T: io::Read + io::Seek, 6 | { 7 | inner: T, 8 | offset: u64, 9 | 10 | initial_skip: bool, 11 | } 12 | 13 | impl Skip 14 | where 15 | T: io::Read + io::Seek, 16 | { 17 | pub fn new(inner: T, offset: u64) -> Self { 18 | Skip { 19 | inner, 20 | offset, 21 | initial_skip: false, 22 | } 23 | } 24 | } 25 | 26 | impl io::Read for Skip 27 | where 28 | T: io::Read + io::Seek, 29 | { 30 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 31 | if !self.initial_skip { 32 | self.inner.seek(io::SeekFrom::Start(self.offset))?; 33 | self.initial_skip = true; 34 | } 35 | self.inner.read(buf) 36 | } 37 | } 38 | 39 | impl io::Seek for Skip 40 | where 41 | T: io::Read + io::Seek, 42 | { 43 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 44 | // TODO: prevent seeking before self.offset. 45 | let new_pos = match pos { 46 | io::SeekFrom::Start(offset) => io::SeekFrom::Start(self.offset + offset), 47 | io::SeekFrom::End(offset) => io::SeekFrom::End(offset), 48 | io::SeekFrom::Current(offset) => io::SeekFrom::Current(offset), 49 | }; 50 | Ok(self.inner.seek(new_pos)? - self.offset) 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | use std::io::Read; 58 | 59 | #[test] 60 | fn skip() { 61 | let file: Vec = (0..16).collect(); 62 | let mut skip = Skip::new(io::Cursor::new(file), 8); 63 | 64 | let mut buf = [0; 16]; 65 | let nread = skip.read(&mut buf).unwrap(); 66 | assert_eq!(nread, 8); 67 | assert_eq!( 68 | &buf, 69 | &[8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0] 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | mod filesystem; 7 | mod id3tag; 8 | mod ioutil; 9 | mod mapping; 10 | mod mp3; 11 | mod soundcloud; 12 | 13 | use self::filesystem::*; 14 | use self::mapping::*; 15 | use log::*; 16 | use std::ffi::OsStr; 17 | use std::process; 18 | 19 | fn main() { 20 | env_logger::init(); 21 | 22 | let cli = clap::App::new("SoundCloud FS") 23 | .version("0.1.0") 24 | .author("polyfloyd ") 25 | .about("A FUSE driver for SoundCloud audio") 26 | .arg( 27 | clap::Arg::with_name("path") 28 | .short("p") 29 | .long("path") 30 | .takes_value(true) 31 | .required(true) 32 | .help("Sets the target directory of the mount"), 33 | ).arg( 34 | clap::Arg::with_name("user") 35 | .short("u") 36 | .long("user") 37 | .takes_value(true) 38 | .required(true) 39 | .multiple(true) 40 | .help("Sets the user to create directory and file entries for"), 41 | ).arg( 42 | clap::Arg::with_name("login") 43 | .long("login") 44 | .value_name("username:password") 45 | .takes_value(true) 46 | .validator(|s| match s.splitn(2, ':').count() { 47 | 2 => Ok(()), 48 | c => Err(format!("bad credential format, split on : yields {} strings", c)), 49 | }).help("Logs in using a username and password instead of accessing the API anonymously"), 50 | ).arg( 51 | clap::Arg::with_name("mpeg-padding") 52 | .long("mpeg-padding") 53 | .value_name("enable") 54 | .takes_value(true) 55 | .default_value("1") 56 | .possible_values(&["0", "1"]) 57 | .help("Enables rewriting parts of the MPEG stream to speed up indexing of media libraries"), 58 | ).arg( 59 | clap::Arg::with_name("id3-images") 60 | .long("id3-images") 61 | .value_name("enable") 62 | .takes_value(true) 63 | .default_value("0") 64 | .possible_values(&["0", "1"]) 65 | .help("Enables image metadata in ID3 tags. This will incur an additional HTTP request everytime a file is opened for reading"), 66 | ).arg( 67 | clap::Arg::with_name("id3-parse-strings") 68 | .long("id3-parse-strings") 69 | .value_name("enable") 70 | .takes_value(true) 71 | .default_value("1") 72 | .possible_values(&["0", "1"]) 73 | .help("Looks into common patterns in track metadata to attempt to determine more accurate ID3 metadata"), 74 | ).get_matches(); 75 | 76 | let login = cli.value_of("login").and_then(|s| { 77 | let mut i = s.splitn(2, ':'); 78 | let u = i.next().unwrap(); 79 | i.next().map(|p| (u, p)) 80 | }); 81 | let sc_client_rs = match login { 82 | None => { 83 | info!("creating anonymous client"); 84 | soundcloud::Client::anonymous() 85 | } 86 | Some((username, password)) => { 87 | info!("logging in as {}", username); 88 | soundcloud::Client::login(&username, password) 89 | } 90 | }; 91 | 92 | let sc_client = match sc_client_rs { 93 | Ok(v) => v, 94 | Err(err) => { 95 | error!("could not initialize SoundCloud client: {}", err); 96 | process::exit(1); 97 | } 98 | }; 99 | 100 | let root = RootState { 101 | sc_client, 102 | show: cli.values_of("user").unwrap().map(str::to_string).collect(), 103 | mpeg_padding: cli.value_of("mpeg-padding") == Some("1"), 104 | id3_download_images: cli.value_of("id3-images") == Some("1"), 105 | id3_parse_strings: cli.value_of("id3-parse-strings") == Some("1"), 106 | }; 107 | 108 | let uid = nix::unistd::Uid::current().as_raw() as u32; 109 | let gid = nix::unistd::Gid::current().as_raw() as u32; 110 | 111 | let fs = FS::new(&CacheRoot::new(&Root::new(&root)), uid, gid); 112 | let path = cli.value_of("path").unwrap(); 113 | let options = &[OsStr::new("-oallow_other"), OsStr::new("-oauto_unmount")]; 114 | fuse::mount(fs, &path, options).unwrap(); 115 | } 116 | -------------------------------------------------------------------------------- /src/mapping.rs: -------------------------------------------------------------------------------- 1 | use crate::filesystem; 2 | use crate::id3tag::tag_for_track; 3 | use crate::ioutil::{Concat, LazyOpen, ReadSeek, Skip}; 4 | use crate::mp3; 5 | use crate::soundcloud; 6 | use chrono::Utc; 7 | use id3; 8 | use std::error; 9 | use std::fmt; 10 | use std::io::{self, Seek}; 11 | use std::path::PathBuf; 12 | 13 | const PADDING_START: u64 = 500; 14 | const PADDING_END: u64 = 20; 15 | 16 | #[derive(Debug)] 17 | pub enum Error { 18 | ChildNotFound, 19 | 20 | SoundCloudError(soundcloud::Error), 21 | IOError(io::Error), 22 | ID3Error(id3::Error), 23 | } 24 | 25 | impl filesystem::Error for Error { 26 | fn not_found() -> Self { 27 | Error::ChildNotFound 28 | } 29 | 30 | fn errno(&self) -> i32 { 31 | match self { 32 | Error::ChildNotFound => libc::ENOENT, 33 | Error::SoundCloudError(_) => libc::EIO, 34 | Error::IOError(err) => err.raw_os_error().unwrap_or(libc::EIO), 35 | Error::ID3Error(_) => libc::EIO, 36 | } 37 | } 38 | } 39 | 40 | impl fmt::Display for Error { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!(f, "{:?}", self) 43 | } 44 | } 45 | 46 | impl error::Error for Error {} 47 | 48 | impl From for Error { 49 | fn from(err: soundcloud::Error) -> Error { 50 | Error::SoundCloudError(err) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(err: io::Error) -> Error { 56 | Error::IOError(err) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(err: id3::Error) -> Error { 62 | Error::ID3Error(err) 63 | } 64 | } 65 | 66 | // TODO: Use proper lifetimes to share state and make this private. 67 | #[derive(Clone)] 68 | pub struct RootState { 69 | pub sc_client: soundcloud::Client, 70 | pub show: Vec, 71 | pub mpeg_padding: bool, 72 | pub id3_download_images: bool, 73 | pub id3_parse_strings: bool, 74 | } 75 | 76 | #[derive(Clone)] 77 | pub struct Root<'a> { 78 | inner: &'a RootState, 79 | } 80 | 81 | impl<'a> Root<'a> { 82 | pub fn new(inner: &'a RootState) -> Self { 83 | Root { inner } 84 | } 85 | } 86 | 87 | impl<'a> filesystem::NodeType for Root<'a> { 88 | type Error = Error; 89 | type File = TrackAudio<'a>; 90 | type Directory = Dir<'a>; 91 | type Symlink = UserReference; 92 | 93 | fn root(&self) -> Self::Directory { 94 | Dir::UserList(UserList { inner: &self.inner }) 95 | } 96 | } 97 | 98 | // Clippy complains about a large size difference in the enum variants. This is because the 99 | // UserList is the only variant that does not has a user field. The warning has been silenced 100 | // because the UserList variant will only be instantiated once. 101 | #[allow(clippy::large_enum_variant)] 102 | #[derive(Clone)] 103 | pub enum Dir<'a> { 104 | UserList(UserList<'a>), 105 | UserProfile(UserProfile<'a>), 106 | UserFavorites(UserFavorites<'a>), 107 | UserFollowing(UserFollowing<'a>), 108 | } 109 | 110 | impl filesystem::Meta for Dir<'_> { 111 | type Error = Error; 112 | fn metadata(&self) -> Result { 113 | match self { 114 | Dir::UserList(f) => f.metadata(), 115 | Dir::UserProfile(f) => f.metadata(), 116 | Dir::UserFavorites(f) => f.metadata(), 117 | Dir::UserFollowing(f) => f.metadata(), 118 | } 119 | } 120 | } 121 | 122 | impl<'a> filesystem::Directory> for Dir<'a> { 123 | fn files(&self) -> Result>)>, Self::Error> { 124 | match self { 125 | Dir::UserList(f) => f.files(), 126 | Dir::UserProfile(f) => f.files(), 127 | Dir::UserFavorites(f) => f.files(), 128 | Dir::UserFollowing(f) => f.files(), 129 | } 130 | } 131 | 132 | fn file_by_name(&self, name: &str) -> Result>, Self::Error> { 133 | if !is_valid_file(name) { 134 | return Err(Error::ChildNotFound); 135 | } 136 | match self { 137 | Dir::UserList(f) => f.file_by_name(name), 138 | Dir::UserProfile(f) => f.file_by_name(name), 139 | Dir::UserFavorites(f) => f.file_by_name(name), 140 | Dir::UserFollowing(f) => f.file_by_name(name), 141 | } 142 | } 143 | } 144 | 145 | #[derive(Clone)] 146 | pub struct UserList<'a> { 147 | inner: &'a RootState, 148 | } 149 | 150 | impl filesystem::Meta for UserList<'_> { 151 | type Error = Error; 152 | fn metadata(&self) -> Result { 153 | let now = Utc::now(); 154 | Ok(filesystem::Metadata { 155 | mtime: now, 156 | ctime: now, 157 | perm: 0o555, 158 | }) 159 | } 160 | } 161 | 162 | impl<'a> filesystem::Directory> for UserList<'a> { 163 | fn files(&self) -> Result>)>, Self::Error> { 164 | self.inner 165 | .show 166 | .iter() 167 | .map(|name| { 168 | let entry = filesystem::Node::Directory(Dir::UserProfile(UserProfile { 169 | inner: &self.inner, 170 | user: soundcloud::User::by_name(&self.inner.sc_client, name)?, 171 | recurse: true, 172 | })); 173 | Ok((name.clone(), entry)) 174 | }) 175 | .collect() 176 | } 177 | 178 | fn file_by_name(&self, name: &str) -> Result>, Self::Error> { 179 | if name.contains('.') { 180 | return Err(Error::ChildNotFound); 181 | } 182 | let entry = filesystem::Node::Directory(Dir::UserProfile(UserProfile { 183 | inner: &self.inner, 184 | user: soundcloud::User::by_name(&self.inner.sc_client, name)?, 185 | recurse: self.inner.show.iter().any(|n| n == name), 186 | })); 187 | Ok(entry) 188 | } 189 | } 190 | 191 | #[derive(Clone)] 192 | pub struct UserFavorites<'a> { 193 | inner: &'a RootState, 194 | user: soundcloud::User, 195 | } 196 | 197 | impl filesystem::Meta for UserFavorites<'_> { 198 | type Error = Error; 199 | fn metadata(&self) -> Result { 200 | Ok(filesystem::Metadata { 201 | mtime: self.user.last_modified, 202 | ctime: self.user.last_modified, 203 | perm: 0o555, 204 | }) 205 | } 206 | } 207 | 208 | impl<'a> filesystem::Directory> for UserFavorites<'a> { 209 | fn files(&self) -> Result>)>, Self::Error> { 210 | let files: Vec<_> = self 211 | .user 212 | .favorites(&self.inner.sc_client)? 213 | .into_iter() 214 | .map(|track| { 215 | ( 216 | format!("{}_-_{}.mp3", track.user.permalink, track.permalink), 217 | filesystem::Node::File(TrackAudio { 218 | inner: self.inner, 219 | track, 220 | }), 221 | ) 222 | }) 223 | .collect(); 224 | Ok(files) 225 | } 226 | } 227 | 228 | #[derive(Clone)] 229 | pub struct UserFollowing<'a> { 230 | inner: &'a RootState, 231 | user: soundcloud::User, 232 | } 233 | 234 | impl filesystem::Meta for UserFollowing<'_> { 235 | type Error = Error; 236 | fn metadata(&self) -> Result { 237 | Ok(filesystem::Metadata { 238 | mtime: self.user.last_modified, 239 | ctime: self.user.last_modified, 240 | perm: 0o555, 241 | }) 242 | } 243 | } 244 | 245 | impl<'a> filesystem::Directory> for UserFollowing<'a> { 246 | fn files(&self) -> Result>)>, Self::Error> { 247 | let files: Vec<_> = self 248 | .user 249 | .following(&self.inner.sc_client)? 250 | .into_iter() 251 | .map(|user| { 252 | ( 253 | user.permalink.clone(), 254 | filesystem::Node::Symlink(UserReference { user }), 255 | ) 256 | }) 257 | .collect(); 258 | Ok(files) 259 | } 260 | } 261 | 262 | #[derive(Clone)] 263 | pub struct UserProfile<'a> { 264 | inner: &'a RootState, 265 | user: soundcloud::User, 266 | // Only add child directories for users marked for recursing, to prevent recursing too deeply. 267 | recurse: bool, 268 | } 269 | 270 | impl<'a> UserProfile<'a> { 271 | fn favorites(&self) -> filesystem::Node> { 272 | filesystem::Node::Directory(Dir::UserFavorites(UserFavorites { 273 | user: self.user.clone(), 274 | inner: self.inner, 275 | })) 276 | } 277 | 278 | fn following(&self) -> filesystem::Node> { 279 | filesystem::Node::Directory(Dir::UserFollowing(UserFollowing { 280 | inner: self.inner, 281 | user: self.user.clone(), 282 | })) 283 | } 284 | } 285 | 286 | impl filesystem::Meta for UserProfile<'_> { 287 | type Error = Error; 288 | fn metadata(&self) -> Result { 289 | Ok(filesystem::Metadata { 290 | mtime: self.user.last_modified, 291 | ctime: self.user.last_modified, 292 | perm: 0o555, 293 | }) 294 | } 295 | } 296 | 297 | impl<'a> filesystem::Directory> for UserProfile<'a> { 298 | fn files(&self) -> Result>)>, Self::Error> { 299 | let mut files = Vec::new(); 300 | if self.recurse { 301 | files.push(("favorites".to_string(), self.favorites())); 302 | files.push(("following".to_string(), self.following())); 303 | } 304 | let tracks = self 305 | .user 306 | .tracks(&self.inner.sc_client)? 307 | .into_iter() 308 | .map(|track| { 309 | ( 310 | format!("{}.mp3", track.permalink), 311 | filesystem::Node::File(TrackAudio { 312 | inner: self.inner, 313 | track, 314 | }), 315 | ) 316 | }); 317 | files.extend(tracks); 318 | Ok(files) 319 | } 320 | 321 | fn file_by_name(&self, name: &str) -> Result>, Self::Error> { 322 | match name { 323 | "favorites" => return Ok(self.favorites()), 324 | "following" => return Ok(self.following()), 325 | _ => (), 326 | } 327 | 328 | let track_pl = name.trim_end_matches(".mp3"); 329 | let track = 330 | soundcloud::Track::by_permalink(&self.inner.sc_client, &self.user.permalink, track_pl)?; 331 | Ok(filesystem::Node::File(TrackAudio { 332 | inner: self.inner, 333 | track, 334 | })) 335 | } 336 | } 337 | 338 | #[derive(Clone)] 339 | pub struct TrackAudio<'a> { 340 | inner: &'a RootState, 341 | track: soundcloud::Track, 342 | } 343 | 344 | impl filesystem::Meta for TrackAudio<'_> { 345 | type Error = Error; 346 | fn metadata(&self) -> Result { 347 | Ok(filesystem::Metadata { 348 | mtime: self.track.last_modified, 349 | ctime: self.track.last_modified, 350 | perm: 0o444, 351 | }) 352 | } 353 | } 354 | 355 | impl<'a> filesystem::File for TrackAudio<'a> { 356 | type Reader = Concat>; 357 | 358 | fn open_ro(&self) -> Result { 359 | let id3_tag = tag_for_track( 360 | &self.track, 361 | self.inner.id3_download_images, 362 | self.inner.id3_parse_strings, 363 | )?; 364 | 365 | let remote_mp3_size = self.track.audio_size() as u64; 366 | let padding_len = mp3::ZERO_FRAME.len() as u64; 367 | let mp3_total_size = 368 | remote_mp3_size + PADDING_START * padding_len + PADDING_END * padding_len; 369 | let mp3_header = mp3::cbr_header(mp3_total_size); 370 | let first_frame_size = mp3_header.len() as u64; 371 | 372 | // Hackety hack: the file concatenation abstraction is able to lazily index the 373 | // size of the underlying files. This ensures for programs that just want to probe 374 | // the audio file's metadata, no request for the actual audio file will be 375 | // performed. 376 | // However, because reading programs may read beyond the metadata, the audio may 377 | // still be accessed. To counter this, we jam a very large swath of zero bytes in 378 | // between the metadata and audio stream to saturate the read buffer without the 379 | // audio stream. 380 | let padding_start = mp3::zero_frames(PADDING_START); 381 | // We also need some padding at the end for players that try to 382 | // read ID3v1 metadata. 383 | let padding_end = mp3::zero_frames(PADDING_END); 384 | 385 | let track_cp = self.track.clone(); 386 | let sc_client_cp = &self.inner.sc_client; 387 | let audio = LazyOpen::with_size_hint(remote_mp3_size, move || { 388 | let f = track_cp 389 | .audio(sc_client_cp) 390 | .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{}", err)))?; 391 | Ok(Skip::new(f, first_frame_size)) 392 | }); 393 | 394 | let concat = if self.inner.mpeg_padding { 395 | Concat::new(vec![ 396 | Box::::from(Box::new(id3_tag)), 397 | Box::::from(Box::new(io::Cursor::new(mp3_header))), 398 | Box::::from(Box::new(padding_start)), 399 | Box::::from(Box::new(audio)), 400 | Box::::from(Box::new(padding_end)), 401 | ]) 402 | } else { 403 | Concat::new(vec![ 404 | Box::::from(Box::new(id3_tag)), 405 | Box::::from(Box::new(audio)), 406 | ]) 407 | }; 408 | Ok(concat) 409 | } 410 | 411 | fn size(&self) -> Result { 412 | let id3_tag_size = { 413 | let mut b = tag_for_track( 414 | &self.track, 415 | self.inner.id3_download_images, 416 | self.inner.id3_parse_strings, 417 | )?; 418 | b.seek(io::SeekFrom::End(0)).unwrap() 419 | }; 420 | let padding_size = if self.inner.mpeg_padding { 421 | let padding_len = mp3::ZERO_FRAME.len() as u64; 422 | PADDING_START * padding_len + PADDING_END * padding_len 423 | } else { 424 | 0 425 | }; 426 | Ok(id3_tag_size + padding_size + self.track.audio_size() as u64) 427 | } 428 | } 429 | 430 | #[derive(Clone)] 431 | pub struct UserReference { 432 | user: soundcloud::User, 433 | } 434 | 435 | impl filesystem::Meta for UserReference { 436 | type Error = Error; 437 | fn metadata(&self) -> Result { 438 | Ok(filesystem::Metadata { 439 | mtime: self.user.last_modified, 440 | ctime: self.user.last_modified, 441 | perm: 0o444, 442 | }) 443 | } 444 | } 445 | 446 | impl filesystem::Symlink for UserReference { 447 | fn read_link(&self) -> Result { 448 | Ok(["..", "..", &self.user.permalink].iter().collect()) 449 | } 450 | } 451 | 452 | fn is_valid_file(name: impl AsRef) -> bool { 453 | match name.as_ref() { 454 | "AACS" | "BACKUP" | "PLAYLIST" | "BDMV" | "bdmv" => false, 455 | name if name.starts_with('.') => false, 456 | _ => true, 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/mp3.rs: -------------------------------------------------------------------------------- 1 | use crate::ioutil; 2 | use lazy_static::lazy_static; 3 | use std::io; 4 | 5 | const FRAMES_FLAG: u32 = 0x0000_0001; 6 | const BYTES_FLAG: u32 = 0x0000_0002; 7 | //const TOC_FLAG: u32 = 0x0000_0004; 8 | //const VBR_SCALE_FLAG: u32 = 0x0000_0008; 9 | 10 | const MEAN_FRAME_SIZE: u64 = 417; 11 | 12 | lazy_static! { 13 | pub static ref ZERO_FRAME: [u8; MEAN_FRAME_SIZE as usize] = { 14 | let mut buf = [0; MEAN_FRAME_SIZE as usize]; 15 | buf[0x00..0x04].copy_from_slice(&[0xff, 0xfb, 0x90, 0x64]); 16 | buf 17 | }; 18 | } 19 | 20 | pub fn zero_frames(count: u64) -> impl io::Read + io::Seek { 21 | ioutil::Pattern::new(&ZERO_FRAME[..], ZERO_FRAME.len() as u64 * count) 22 | } 23 | 24 | pub fn cbr_header(bytes: u64) -> Vec { 25 | let mut buf = vec![0; 417]; 26 | 27 | // The MPEG header. 28 | buf[0x00..0x04].copy_from_slice(&[0xff, 0xfb, 0x90, 0x64]); 29 | 30 | // "Info" to indicate that this is a header for a CBR stream. 31 | buf[0x24..0x28].copy_from_slice(b"Info"); 32 | 33 | // Header flags. 34 | let flags = FRAMES_FLAG | BYTES_FLAG; 35 | buf[0x28..0x2c].copy_from_slice(&flags.to_be_bytes()); 36 | 37 | // 0x34..0x98: Table of contents used for seeking. Not relevant for CBR. 38 | 39 | // The number of frames in the file. 40 | if flags & FRAMES_FLAG != 0 { 41 | let frames = bytes / MEAN_FRAME_SIZE; 42 | assert!(frames <= u64::from(std::u32::MAX)); 43 | buf[0x2c..0x30].copy_from_slice(&(frames as u32).to_be_bytes()); 44 | } 45 | 46 | // The filesize in bytes. 47 | if flags & BYTES_FLAG != 0 { 48 | assert!(bytes <= u64::from(std::u32::MAX)); 49 | buf[0x30..0x34].copy_from_slice(&(bytes as u32).to_be_bytes()); 50 | } 51 | 52 | // 0x34..0x38: VBR scale, whatever that is. 53 | 54 | // There are also the enc_delay and enc_padding fields, we'll leave them 0. 55 | 56 | // The encoder version string. Usually, this is something like "LAME3.99". 57 | let encoder = concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION")); 58 | copy_from_var_str(&mut buf[0x9c..0xb0], &encoder); 59 | 60 | buf 61 | } 62 | 63 | fn copy_from_var_str(buf: &mut [u8], s: &str) { 64 | let b = s.as_bytes(); 65 | buf[..b.len()].copy_from_slice(b); 66 | } 67 | 68 | // 00000000: fffb 9064 0000 0000 0000 0000 0000 0000 ...d............ 69 | // 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 70 | // 00000020: 0000 0000 496e 666f 0000 000f 0000 25bf ....Info......%. 71 | // 00000030: 003d a1f4 0003 0508 0b0d 1012 1517 1a1c .=.............. 72 | // 00000040: 1f21 2426 292c 2e31 3336 383b 3d40 4245 .!$&),.1368;=@BE 73 | // 00000050: 484a 4d4f 5254 5759 5c5e 6164 6669 6b6e HJMORTWY\^adfikn 74 | // 00000060: 7073 7578 7a7d 8082 8587 8a8c 8f91 9496 psuxz}.......... 75 | // 00000070: 999b 9ea1 a3a6 a8ab adb0 b2b5 b7ba bdbf ................ 76 | // 00000080: c2c4 c7c9 ccce d1d3 d6d9 dbde e0e3 e5e8 ................ 77 | // 00000090: eaed eff2 f5f7 fafc 0000 0039 4c41 4d45 ...........9LAME 78 | // 000000a0: 332e 3939 7201 aa00 0000 002e 4800 0014 3.99r.......H... 79 | // 000000b0: 8024 049b 4e00 0080 003d a1f4 ed2b 38fc .$..N....=...+8. 80 | // 000000c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 81 | // 000000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 82 | // 000000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 83 | // 000000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 84 | // 00000100: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 85 | // 00000110: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 86 | // 00000120: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 87 | // 00000130: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 88 | // 00000140: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 89 | // 00000150: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 90 | // 00000160: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 91 | // 00000170: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 92 | // 00000180: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 93 | // 00000190: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 94 | // 000001a0: 00 . 95 | -------------------------------------------------------------------------------- /src/soundcloud/error.rs: -------------------------------------------------------------------------------- 1 | use reqwest; 2 | use std::error; 3 | use std::fmt; 4 | use std::io; 5 | 6 | #[derive(Debug)] 7 | pub enum Error { 8 | Login, 9 | ArtworkNotAvailable, 10 | 11 | IOError(io::Error), 12 | 13 | ReqwestError(reqwest::Error), 14 | ReqwestInvalidHeader(reqwest::header::InvalidHeaderValue), 15 | ReqwestUrlParseError(url::ParseError), 16 | 17 | MalformedResponse { 18 | method: reqwest::Method, 19 | url: reqwest::Url, 20 | body: String, 21 | error: Box, 22 | }, 23 | 24 | Generic(String), 25 | } 26 | 27 | impl fmt::Display for Error { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | write!(f, "{:?}", self) 30 | } 31 | } 32 | 33 | impl error::Error for Error {} 34 | 35 | impl From for Error { 36 | fn from(err: io::Error) -> Self { 37 | Self::IOError(err) 38 | } 39 | } 40 | 41 | impl From for Error { 42 | fn from(err: reqwest::Error) -> Self { 43 | Self::ReqwestError(err) 44 | } 45 | } 46 | 47 | impl From for Error { 48 | fn from(err: reqwest::header::InvalidHeaderValue) -> Self { 49 | Self::ReqwestInvalidHeader(err) 50 | } 51 | } 52 | 53 | impl From for Error { 54 | fn from(err: url::ParseError) -> Self { 55 | Self::ReqwestUrlParseError(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/soundcloud/format.rs: -------------------------------------------------------------------------------- 1 | use serde::{self, Deserialize, Deserializer}; 2 | 3 | pub mod date { 4 | use super::*; 5 | use chrono::{DateTime, TimeZone, Utc}; 6 | 7 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 8 | where 9 | D: Deserializer<'de>, 10 | { 11 | let s = String::deserialize(deserializer)?; 12 | Utc.datetime_from_str(&s, "%Y/%m/%d %H:%M:%S %z") 13 | .map_err(serde::de::Error::custom) 14 | } 15 | } 16 | 17 | pub mod empty_str_as_none { 18 | use super::*; 19 | 20 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 21 | where 22 | D: Deserializer<'de>, 23 | { 24 | let o: Option = Option::deserialize(deserializer)?; 25 | Ok(o.filter(|s| !s.is_empty())) 26 | } 27 | } 28 | 29 | pub mod null_as_false { 30 | use super::*; 31 | 32 | pub fn deserialize<'de, D>(deserializer: D) -> Result 33 | where 34 | D: Deserializer<'de>, 35 | { 36 | let o: Option = Option::deserialize(deserializer)?; 37 | Ok(o.unwrap_or(false)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/soundcloud/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod format; 3 | mod track; 4 | mod user; 5 | mod util; 6 | 7 | use self::util::http::retry_execute; 8 | use lazy_static::lazy_static; 9 | use log::*; 10 | use rayon::prelude::*; 11 | use regex::bytes::Regex; 12 | use reqwest::blocking::{self, RequestBuilder}; 13 | use reqwest::{header, Method, Url}; 14 | use serde::de::DeserializeOwned; 15 | use std::fmt; 16 | use std::str; 17 | use url; 18 | 19 | pub use self::error::Error; 20 | pub use self::track::Track; 21 | pub use self::user::User; 22 | 23 | const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0"; 24 | const PAGE_MAX_SIZE: u64 = 200; 25 | 26 | pub(crate) fn default_headers() -> header::HeaderMap { 27 | let mut headers = header::HeaderMap::new(); 28 | headers.insert( 29 | header::USER_AGENT, 30 | header::HeaderValue::from_static(USER_AGENT), 31 | ); 32 | headers.insert( 33 | header::REFERER, 34 | header::HeaderValue::from_static("https://soundcloud.com/"), 35 | ); 36 | headers.insert( 37 | header::ORIGIN, 38 | header::HeaderValue::from_static("https://soundcloud.com/"), 39 | ); 40 | headers 41 | } 42 | 43 | pub(crate) fn default_client() -> &'static blocking::Client { 44 | lazy_static! { 45 | static ref DEFAULT_CLIENT: blocking::Client = blocking::Client::builder() 46 | .default_headers(default_headers()) 47 | .build() 48 | .unwrap(); 49 | } 50 | &DEFAULT_CLIENT 51 | } 52 | 53 | #[derive(Clone)] 54 | pub struct Client { 55 | client: blocking::Client, 56 | client_id: String, 57 | token: Option, 58 | } 59 | 60 | impl Client { 61 | /// Set up a client by logging in using the online form, just like a user would in the web 62 | /// application. 63 | /// 64 | /// This login method is not guaranteed to be stable! 65 | pub fn login(username: impl AsRef, password: impl AsRef) -> Result { 66 | let client = default_client(); 67 | let client_id = anonymous_client_id(&client)?; 68 | 69 | let token = { 70 | trace!("performing password login with user: {}", username.as_ref()); 71 | let login_req_body = PasswordLoginReqBody { 72 | client_id: &client_id, 73 | scope: "fast-connect non-expiring purchase signup upload", 74 | recaptcha_pubkey: "6LeAxT8UAAAAAOLTfaWhndPCjGOnB54U1GEACb7N", 75 | recaptcha_response: None, 76 | credentials: Credentials { 77 | identifier: username.as_ref(), 78 | password: password.as_ref(), 79 | }, 80 | signature: "8:3-1-28405-134-1638720-1024-0-0:4ab691:2", 81 | device_id: "381629-667600-267798-887023", 82 | user_agent: USER_AGENT, 83 | }; 84 | let login_url = Url::parse_with_params( 85 | "https://api-v2.soundcloud.com/sign-in/password?app_version=1541509103&app_locale=en", 86 | &[("client_id", &client_id)], 87 | ).unwrap(); 88 | trace!("password login URL: {}", login_url); 89 | let login_res_body: PasswordLoginResBody = retry_execute( 90 | client, 91 | client.post(login_url).json(&login_req_body).build()?, 92 | )? 93 | .error_for_status()? 94 | .json()?; 95 | login_res_body.session.access_token 96 | }; 97 | 98 | trace!("SoundCloud login got token: {}****", &token[0..4]); 99 | Client::from_token(client_id, token) 100 | } 101 | 102 | // Attempt to create a client with read-only access to the public API. 103 | pub fn anonymous() -> Result { 104 | let client = default_client(); 105 | let client_id = anonymous_client_id(&client)?; 106 | Ok(Client { 107 | client: client.clone(), 108 | client_id, 109 | token: None, 110 | }) 111 | } 112 | 113 | fn from_token(client_id: impl Into, token: impl Into) -> Result { 114 | let token = token.into(); 115 | let auth_client = blocking::Client::builder() 116 | .default_headers({ 117 | let auth_header = format!("OAuth {}", token).parse()?; 118 | let mut headers = default_headers(); 119 | headers.insert(header::AUTHORIZATION, auth_header); 120 | headers 121 | }) 122 | .build()?; 123 | Ok(Client { 124 | client: auth_client, 125 | client_id: client_id.into(), 126 | token: Some(token), 127 | }) 128 | } 129 | 130 | pub(crate) fn request( 131 | &self, 132 | method: reqwest::Method, 133 | base_url: impl AsRef, 134 | ) -> Result<(RequestBuilder, Url), Error> { 135 | let url = Url::parse_with_params(base_url.as_ref(), &[("client_id", &self.client_id)])?; 136 | let req = self.client.request(method, url.clone()); 137 | Ok((req, url)) 138 | } 139 | 140 | pub(crate) fn query_string( 141 | &self, 142 | method: reqwest::Method, 143 | base_url: impl AsRef, 144 | ) -> Result { 145 | let (req, url) = self.request(method.clone(), base_url)?; 146 | info!("querying {} {}", method, url); 147 | let s = retry_execute(&self.client, req.build()?)? 148 | .error_for_status()? 149 | .text()?; 150 | Ok(s) 151 | } 152 | 153 | pub(crate) fn query( 154 | &self, 155 | method: reqwest::Method, 156 | base_url: impl AsRef, 157 | ) -> Result { 158 | let (req, url) = self.request(method.clone(), base_url)?; 159 | info!("querying {} {}", method, url); 160 | let mut buf = Vec::new(); 161 | retry_execute(&self.client, req.build()?)? 162 | .error_for_status()? 163 | .copy_to(&mut buf)?; 164 | 165 | match serde_json::from_slice(&buf[..]) { 166 | Ok(t) => Ok(t), 167 | Err(err) => { 168 | let body = String::from_utf8_lossy(&buf[..]); 169 | warn!("bad body: {}", body); 170 | warn!("bad body error: {}", err); 171 | Err(Error::MalformedResponse { 172 | method, 173 | url, 174 | body: body.to_string(), 175 | error: Box::new(err), 176 | }) 177 | } 178 | } 179 | } 180 | } 181 | 182 | impl fmt::Debug for Client { 183 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 184 | let token = self 185 | .token 186 | .as_ref() 187 | .filter(|t| t.len() >= 4) 188 | .map(|t| format!("{}****", &t[0..4])) 189 | .unwrap_or_else(|| "".to_string()); 190 | write!(f, "Client {{ id: {}, token: {} }}", self.client_id, token) 191 | } 192 | } 193 | 194 | fn anonymous_client_id(client: &blocking::Client) -> Result { 195 | lazy_static! { 196 | static ref RE_SCRIPT_TAG: Regex = 197 | Regex::new("").unwrap(); 198 | static ref RE_CLIENT_ID: Regex = Regex::new("client_id:\"(.+?)\"").unwrap(); 199 | } 200 | 201 | // Find the last