├── .env ├── .gitignore ├── LICENSE.md ├── Pipfile ├── Pipfile.lock ├── README.md ├── fediplay ├── __init__.py ├── __main__.py ├── cli.py ├── dirs.py ├── env.py ├── keyring.py ├── mastodon.py └── queue.py ├── setup.py └── test_fediplay.py /.env: -------------------------------------------------------------------------------- 1 | #FEDIPLAY_NO_CHECK_CERTIFICATE=1 2 | FEDIPLAY_PLAY_COMMAND="ffplay -v 0 -nostats -hide_banner -autoexit -nodisp {filename}" 3 | #FEDIPLAY_PLAY_COMMAND="afplay {filename}" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.mp3 3 | *.mp4 4 | *.pyc 5 | *.secret 6 | .cache 7 | .idea 8 | .pytest_cache 9 | .vscode 10 | __pycache__ 11 | fediplay.iml 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Matt Behrens. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | cssselect = "*" 8 | lxml = "*" 9 | "mastodon.py" = "*" 10 | youtube-dl = "*" 11 | python-dotenv = "*" 12 | click = "*" 13 | "e1839a8" = {path = ".", editable = true} 14 | appdirs = "*" 15 | keyring = "*" 16 | "keyrings.alt" = "*" 17 | 18 | [dev-packages] 19 | pytest = "*" 20 | pylint = "*" 21 | 22 | [requires] 23 | python_version = "3.7" 24 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "823dc15b2e595d044b28df0bb1d28da5da1f2235baa200cf8d36c3eeb988a86d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "appdirs": { 20 | "hashes": [ 21 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 22 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.3" 26 | }, 27 | "asn1crypto": { 28 | "hashes": [ 29 | "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", 30 | "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" 31 | ], 32 | "version": "==0.24.0" 33 | }, 34 | "certifi": { 35 | "hashes": [ 36 | "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", 37 | "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" 38 | ], 39 | "version": "==2018.10.15" 40 | }, 41 | "cffi": { 42 | "hashes": [ 43 | "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", 44 | "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", 45 | "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", 46 | "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", 47 | "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", 48 | "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", 49 | "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", 50 | "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", 51 | "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", 52 | "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", 53 | "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", 54 | "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", 55 | "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", 56 | "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", 57 | "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", 58 | "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", 59 | "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", 60 | "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", 61 | "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", 62 | "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", 63 | "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", 64 | "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", 65 | "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", 66 | "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", 67 | "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", 68 | "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", 69 | "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", 70 | "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", 71 | "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", 72 | "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", 73 | "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", 74 | "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" 75 | ], 76 | "version": "==1.11.5" 77 | }, 78 | "chardet": { 79 | "hashes": [ 80 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 81 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 82 | ], 83 | "version": "==3.0.4" 84 | }, 85 | "click": { 86 | "hashes": [ 87 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 88 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 89 | ], 90 | "index": "pypi", 91 | "version": "==7.0" 92 | }, 93 | "cryptography": { 94 | "hashes": [ 95 | "sha256:02602e1672b62e803e08617ec286041cc453e8d43f093a5f4162095506bc0beb", 96 | "sha256:10b48e848e1edb93c1d3b797c83c72b4c387ab0eb4330aaa26da8049a6cbede0", 97 | "sha256:17db09db9d7c5de130023657be42689d1a5f60502a14f6f745f6f65a6b8195c0", 98 | "sha256:227da3a896df1106b1a69b1e319dce218fa04395e8cc78be7e31ca94c21254bc", 99 | "sha256:2cbaa03ac677db6c821dac3f4cdfd1461a32d0615847eedbb0df54bb7802e1f7", 100 | "sha256:31db8febfc768e4b4bd826750a70c79c99ea423f4697d1dab764eb9f9f849519", 101 | "sha256:4a510d268e55e2e067715d728e4ca6cd26a8e9f1f3d174faf88e6f2cb6b6c395", 102 | "sha256:6a88d9004310a198c474d8a822ee96a6dd6c01efe66facdf17cb692512ae5bc0", 103 | "sha256:76936ec70a9b72eb8c58314c38c55a0336a2b36de0c7ee8fb874a4547cadbd39", 104 | "sha256:7e3b4aecc4040928efa8a7cdaf074e868af32c58ffc9bb77e7bf2c1a16783286", 105 | "sha256:8168bcb08403ef144ff1fb880d416f49e2728101d02aaadfe9645883222c0aa5", 106 | "sha256:8229ceb79a1792823d87779959184a1bf95768e9248c93ae9f97c7a2f60376a1", 107 | "sha256:8a19e9f2fe69f6a44a5c156968d9fc8df56d09798d0c6a34ccc373bb186cee86", 108 | "sha256:8d10113ca826a4c29d5b85b2c4e045ffa8bad74fb525ee0eceb1d38d4c70dfd6", 109 | "sha256:be495b8ec5a939a7605274b6e59fbc35e76f5ad814ae010eb679529671c9e119", 110 | "sha256:dc2d3f3b1548f4d11786616cf0f4415e25b0fbecb8a1d2cd8c07568f13fdde38", 111 | "sha256:e4aecdd9d5a3d06c337894c9a6e2961898d3f64fe54ca920a72234a3de0f9cb3", 112 | "sha256:e79ab4485b99eacb2166f3212218dd858258f374855e1568f728462b0e6ee0d9", 113 | "sha256:f995d3667301e1754c57b04e0bae6f0fa9d710697a9f8d6712e8cca02550910f" 114 | ], 115 | "version": "==2.3.1" 116 | }, 117 | "cssselect": { 118 | "hashes": [ 119 | "sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204", 120 | "sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206" 121 | ], 122 | "index": "pypi", 123 | "version": "==1.0.3" 124 | }, 125 | "decorator": { 126 | "hashes": [ 127 | "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", 128 | "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" 129 | ], 130 | "version": "==4.3.0" 131 | }, 132 | "e1839a8": { 133 | "editable": true, 134 | "path": "." 135 | }, 136 | "entrypoints": { 137 | "hashes": [ 138 | "sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b", 139 | "sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f" 140 | ], 141 | "version": "==0.2.3" 142 | }, 143 | "http-ece": { 144 | "hashes": [ 145 | "sha256:2f31a0640c31a0c2934ab1e37005dd9a559ae854a16304f9b839e062074106cc" 146 | ], 147 | "version": "==1.0.5" 148 | }, 149 | "idna": { 150 | "hashes": [ 151 | "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", 152 | "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 153 | ], 154 | "version": "==2.7" 155 | }, 156 | "keyring": { 157 | "hashes": [ 158 | "sha256:16dddc3edaeb2703aaf5588a0b488b62f162e26f1877b6faf3a3db4b7712df61", 159 | "sha256:6232b972dfbd44fd9bd649242dbf17f616988b152d4268f9ca1dcc704b467381" 160 | ], 161 | "index": "pypi", 162 | "version": "==15.1.0" 163 | }, 164 | "keyrings.alt": { 165 | "hashes": [ 166 | "sha256:6a00fa799baf1385cf9620bd01bcc815aa56e6970342a567bcfea0c4d21abe5f", 167 | "sha256:b59c86b67b9027a86e841a49efc41025bcc3b1b0308629617b66b7011e52db5a" 168 | ], 169 | "index": "pypi", 170 | "version": "==3.1" 171 | }, 172 | "lxml": { 173 | "hashes": [ 174 | "sha256:02bc220d61f46e9b9d5a53c361ef95e9f5e1d27171cd461dddb17677ae2289a5", 175 | "sha256:22f253b542a342755f6cfc047fe4d3a296515cf9b542bc6e261af45a80b8caf6", 176 | "sha256:2f31145c7ff665b330919bfa44aacd3a0211a76ca7e7b441039d2a0b0451e415", 177 | "sha256:36720698c29e7a9626a0dc802ef8885f8f0239bfd1689628ecd459a061f2807f", 178 | "sha256:438a1b0203545521f6616132bfe0f4bca86f8a401364008b30e2b26ec408ce85", 179 | "sha256:4815892904c336bbaf73dafd54f45f69f4021c22b5bad7332176bbf4fb830568", 180 | "sha256:5be031b0f15ad63910d8e5038b489d95a79929513b3634ad4babf77100602588", 181 | "sha256:5c93ae37c3c588e829b037fdfbd64a6e40c901d3f93f7beed6d724c44829a3ad", 182 | "sha256:60842230678674cdac4a1cf0f707ef12d75b9a4fc4a565add4f710b5fcf185d5", 183 | "sha256:62939a8bb6758d1bf923aa1c13f0bcfa9bf5b2fc0f5fa917a6e25db5fe0cfa4e", 184 | "sha256:75830c06a62fe7b8fe3bbb5f269f0b308f19f3949ac81cfd40062f47c1455faf", 185 | "sha256:81992565b74332c7c1aff6a913a3e906771aa81c9d0c68c68113cffcae45bc53", 186 | "sha256:8c892fb0ee52c594d9a7751c7d7356056a9682674b92cc1c4dc968ff0f30c52f", 187 | "sha256:9d862e3cf4fc1f2837dedce9c42269c8c76d027e49820a548ac89fdcee1e361f", 188 | "sha256:a623965c086a6e91bb703d4da62dabe59fe88888e82c4117d544e11fd74835d6", 189 | "sha256:a7783ab7f6a508b0510490cef9f857b763d796ba7476d9703f89722928d1e113", 190 | "sha256:aab09fbe8abfa3b9ce62aaf45aca2d28726b1b9ee44871dbe644050a2fff4940", 191 | "sha256:abf181934ac3ef193832fb973fd7f6149b5c531903c2ec0f1220941d73eee601", 192 | "sha256:ae07fa0c115733fce1e9da96a3ac3fa24801742ca17e917e0c79d63a01eeb843", 193 | "sha256:b9c78242219f674ab645ec571c9a95d70f381319a23911941cd2358a8e0521cf", 194 | "sha256:bccb267678b870d9782c3b44d0cefe3ba0e329f9af8c946d32bf3778e7a4f271", 195 | "sha256:c4df4d27f4c93b2cef74579f00b1d3a31a929c7d8023f870c4b476f03a274db4", 196 | "sha256:caf0e50b546bb60dfa99bb18dfa6748458a83131ecdceaf5c071d74907e7e78a", 197 | "sha256:d3266bd3ac59ac4edcd5fa75165dee80b94a3e5c91049df5f7c057ccf097551c", 198 | "sha256:db0d213987bcd4e6d41710fb4532b22315b0d8fb439ff901782234456556aed1", 199 | "sha256:dbbd5cf7690a40a9f0a9325ab480d0fccf46d16b378eefc08e195d84299bfae1", 200 | "sha256:e16e07a0ec3a75b5ee61f2b1003c35696738f937dc8148fbda9fe2147ccb6e61", 201 | "sha256:e175a006725c7faadbe69e791877d09936c0ef2cf49d01b60a6c1efcb0e8be6f", 202 | "sha256:edd9c13a97f6550f9da2236126bb51c092b3b1ce6187f2bd966533ad794bbb5e", 203 | "sha256:fa39ea60d527fbdd94215b5e5552f1c6a912624521093f1384a491a8ad89ad8b" 204 | ], 205 | "index": "pypi", 206 | "version": "==4.2.5" 207 | }, 208 | "mastodon.py": { 209 | "hashes": [ 210 | "sha256:3f000f34e2740a90fdadd923d1a7dd100dd62d56906c026ae725ce43301f30f6", 211 | "sha256:b29cb6eb268f65951d22b8cd0afbb233402e15b26bd8568f1c2ef506dbeeb1c0" 212 | ], 213 | "index": "pypi", 214 | "version": "==1.3.1" 215 | }, 216 | "pycparser": { 217 | "hashes": [ 218 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" 219 | ], 220 | "version": "==2.19" 221 | }, 222 | "python-dateutil": { 223 | "hashes": [ 224 | "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", 225 | "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" 226 | ], 227 | "version": "==2.7.3" 228 | }, 229 | "python-dotenv": { 230 | "hashes": [ 231 | "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77", 232 | "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77" 233 | ], 234 | "index": "pypi", 235 | "version": "==0.9.1" 236 | }, 237 | "pytz": { 238 | "hashes": [ 239 | "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", 240 | "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" 241 | ], 242 | "version": "==2018.5" 243 | }, 244 | "requests": { 245 | "hashes": [ 246 | "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", 247 | "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" 248 | ], 249 | "version": "==2.20.0" 250 | }, 251 | "six": { 252 | "hashes": [ 253 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 254 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 255 | ], 256 | "version": "==1.11.0" 257 | }, 258 | "urllib3": { 259 | "hashes": [ 260 | "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae", 261 | "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59" 262 | ], 263 | "version": "==1.24" 264 | }, 265 | "youtube-dl": { 266 | "hashes": [ 267 | "sha256:17b03b94031efc28457ef0808ea2c750012aa38007b2980f93a9c9b1d7387363", 268 | "sha256:2edaa70587e0e3ade46f017b6f705c245c45b9b5c329227f7dfd4823e462e7b2" 269 | ], 270 | "index": "pypi", 271 | "version": "==2018.10.5" 272 | } 273 | }, 274 | "develop": { 275 | "astroid": { 276 | "hashes": [ 277 | "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", 278 | "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" 279 | ], 280 | "version": "==2.0.4" 281 | }, 282 | "atomicwrites": { 283 | "hashes": [ 284 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 285 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 286 | ], 287 | "version": "==1.2.1" 288 | }, 289 | "attrs": { 290 | "hashes": [ 291 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 292 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 293 | ], 294 | "version": "==18.2.0" 295 | }, 296 | "isort": { 297 | "hashes": [ 298 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 299 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 300 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 301 | ], 302 | "version": "==4.3.4" 303 | }, 304 | "lazy-object-proxy": { 305 | "hashes": [ 306 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 307 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 308 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 309 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 310 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 311 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 312 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 313 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 314 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 315 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 316 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 317 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 318 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 319 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 320 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 321 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 322 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 323 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 324 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 325 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 326 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 327 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 328 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 329 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 330 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 331 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 332 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 333 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 334 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 335 | ], 336 | "version": "==1.3.1" 337 | }, 338 | "mccabe": { 339 | "hashes": [ 340 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 341 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 342 | ], 343 | "version": "==0.6.1" 344 | }, 345 | "more-itertools": { 346 | "hashes": [ 347 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", 348 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", 349 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" 350 | ], 351 | "version": "==4.3.0" 352 | }, 353 | "pluggy": { 354 | "hashes": [ 355 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 356 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 357 | ], 358 | "version": "==0.8.0" 359 | }, 360 | "py": { 361 | "hashes": [ 362 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 363 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 364 | ], 365 | "version": "==1.7.0" 366 | }, 367 | "pylint": { 368 | "hashes": [ 369 | "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", 370 | "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" 371 | ], 372 | "index": "pypi", 373 | "version": "==2.1.1" 374 | }, 375 | "pytest": { 376 | "hashes": [ 377 | "sha256:10e59f84267370ab20cec9305bafe7505ba4d6b93ecbf66a1cce86193ed511d5", 378 | "sha256:8c827e7d4816dfe13e9329c8226aef8e6e75d65b939bc74fda894143b6d1df59" 379 | ], 380 | "index": "pypi", 381 | "version": "==3.9.1" 382 | }, 383 | "six": { 384 | "hashes": [ 385 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 386 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 387 | ], 388 | "version": "==1.11.0" 389 | }, 390 | "wrapt": { 391 | "hashes": [ 392 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 393 | ], 394 | "version": "==1.10.11" 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fediplay 2 | 3 | A Mastodon client that automatically plays your friends' music as they toot links to it. 4 | 5 | ## What's new in 2.2 6 | 7 | If you've been using fediplay before, the all-new version 2.2 will be a little different! 8 | 9 | - You now specify the instance you want to stream from on the command line, instead of setting it in the environment. fediplay has been upgraded with the power of [Click](http://click.pocoo.org/) to give it a more modern command-line interface. 10 | 11 | - We use [appdirs](https://pypi.org/project/appdirs/) to keep downloaded music files in your operating system's user cache directory. 12 | 13 | - We use [keyring](https://pypi.org/project/keyring/) to store your client credentials and access token, securely if your operating system supports it. If you already have `.secret` files from an earlier version, we'll migrate them automatically for you. 14 | 15 | Be sure to follow all the instructions, including re-running `pipenv install` to update the installed dependencies. 16 | 17 | ## Getting started 18 | 19 | fediplay comes configured to use `ffplay` from [FFmpeg](https://ffmpeg.org/) to actually play music. 20 | 21 | - On macOS, `ffplay` is part of the [Homebrew](https://brew.sh/) `ffmpeg` package, but you need to build it with `brew install ffmpeg --with-sdl2`. 22 | 23 | - On Windows, `ffplay` is part of the [Scoop](http://scoop.sh/) `ffmpeg` package. 24 | 25 | Use `pipenv install` from [Pipenv](https://docs.pipenv.org/) to install the Python dependencies and set up the fediplay script inside the virtual environment. 26 | 27 | You can use the fediplay script with either `pipenv run fediplay` or by entering the Pipenv shell with `pipenv shell` and just running `fediplay`. 28 | 29 | ## Registering and logging in 30 | 31 | To register fediplay to your instance, use `fediplay register example.com`. 32 | 33 | To log in to your instance, use `fediplay login example.com`. 34 | 35 | ## Streaming 36 | 37 | Use `fediplay stream example.com` to start the stream. You'll need to log in the first time. 38 | 39 | Toots that include the hashtag #fediplay and have as their first link something that [youtube-dl](https://rg3.github.io/youtube-dl/) can play, will! 40 | 41 | If new #fediplay toots come in while music is playing, they'll be downloaded immediately and queued to be played later. 42 | 43 | Since version 2.2, thanks to [@bbonf](https://github.com/bbonf), if there's a recent #fediplay toot in your timeline, it'll be pulled up and played before the stream starts. Great if you just missed a song before starting your stream! 44 | 45 | ### Filtering 46 | 47 | Since version 2.2, you can also, thanks to [@Jenkyrados](https://github.com/Jenkyrados), specify users to filter! Just add them to the command line after the server name, e.g. `fediplay stream example.com @user @otheruser@example.net`. 48 | 49 | -------------------------------------------------------------------------------- /fediplay/__init__.py: -------------------------------------------------------------------------------- 1 | '''A Mastodon client for playing your friends' music.''' 2 | 3 | from fediplay.cli import cli 4 | -------------------------------------------------------------------------------- /fediplay/__main__.py: -------------------------------------------------------------------------------- 1 | '''Hook for running fediplay module as a script.''' 2 | 3 | from fediplay.cli import cli 4 | 5 | cli() 6 | -------------------------------------------------------------------------------- /fediplay/cli.py: -------------------------------------------------------------------------------- 1 | '''Entry point for command-line interface.''' 2 | 3 | options = {'debug': False} 4 | 5 | import os 6 | path = os.path 7 | import sys 8 | 9 | import appdirs 10 | import click 11 | import atexit 12 | from mastodon import Mastodon 13 | 14 | from fediplay.dirs import DIRS 15 | import fediplay.mastodon as mastodon 16 | import fediplay.keyring as keyring 17 | 18 | def ensure_dirs(): 19 | '''Make sure the application directories exist.''' 20 | 21 | if not path.exists(DIRS.user_config_dir): 22 | os.makedirs(DIRS.user_config_dir) 23 | 24 | if not path.exists(DIRS.user_cache_dir): 25 | os.makedirs(DIRS.user_cache_dir) 26 | 27 | def get_access_token(instance): 28 | '''Ensure the user credential exists.''' 29 | 30 | keyring.migrate_access_token(instance) 31 | 32 | if not keyring.has_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN): 33 | click.echo('user credential for {} does not exist; try `fediplay login`'.format(instance)) 34 | sys.exit(1) 35 | 36 | return keyring.get_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN) 37 | 38 | def get_client_credentials(instance): 39 | '''Ensure the client credentials exist.''' 40 | 41 | keyring.migrate_client_credentials(instance) 42 | 43 | if not (keyring.has_credential(instance, keyring.CREDENTIAL_CLIENT_ID) and 44 | keyring.has_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET)): 45 | click.echo('client credentials for {} do not exist; try `fediplay register`'.format(instance)) 46 | sys.exit(1) 47 | 48 | return ( 49 | keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_ID), 50 | keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET) 51 | ) 52 | 53 | @click.group() 54 | @click.option('-d', '--debug', is_flag=True, help='Print debug messages.') 55 | def cli(debug): 56 | '''A program to play music your friends post on Mastodon.''' 57 | 58 | options['debug'] = debug 59 | 60 | ensure_dirs() 61 | 62 | @cli.command() 63 | @click.argument('instance') 64 | def register(instance): 65 | '''Register fediplay on your Mastodon instance.''' 66 | 67 | mastodon.register(instance) 68 | 69 | @cli.command() 70 | @click.argument('instance') 71 | def login(instance): 72 | '''Log in to your Mastodon instance.''' 73 | 74 | client_id, client_secret = get_client_credentials(instance) 75 | 76 | click.echo('Open this page in your browser and follow the instructions.') 77 | click.echo('Paste the code here.') 78 | click.echo('') 79 | click.echo(mastodon.get_auth_request_url(instance, client_id, client_secret)) 80 | click.echo('') 81 | 82 | grant_code = input('Code: ') 83 | mastodon.login(instance, client_id, client_secret, grant_code) 84 | 85 | @cli.command() 86 | @click.argument('instance') 87 | @click.argument('users', nargs=-1) 88 | @click.option('--clean-up-files', is_flag=True) 89 | def stream(instance, users, clean_up_files): 90 | '''Stream music from your timeline.''' 91 | if ( clean_up_files ): 92 | atexit.register(delete_files) 93 | 94 | client_id, client_secret = get_client_credentials(instance) 95 | access_token = get_access_token(instance) 96 | 97 | mastodon.stream(instance, users, client_id, client_secret, access_token, cache_dir=DIRS.user_cache_dir) 98 | 99 | def delete_files(): 100 | cache_dir = DIRS.user_cache_dir 101 | for the_file in os.listdir(cache_dir): 102 | file_path = os.path.join(cache_dir, the_file) 103 | if os.path.isfile(file_path): 104 | os.remove(file_path) 105 | print('deleted ' + the_file) 106 | 107 | @cli.command() 108 | def clean_up_files(): 109 | delete_files() 110 | 111 | 112 | -------------------------------------------------------------------------------- /fediplay/dirs.py: -------------------------------------------------------------------------------- 1 | '''Application directories.''' 2 | 3 | from appdirs import AppDirs 4 | 5 | 6 | DIRS = AppDirs('fediplay', 'zigg') 7 | -------------------------------------------------------------------------------- /fediplay/env.py: -------------------------------------------------------------------------------- 1 | '''Environment variable management.''' 2 | 3 | from os import getenv 4 | 5 | from dotenv import load_dotenv, find_dotenv 6 | 7 | 8 | def no_check_certificate(): 9 | '''Returns whether fediplay should check TLS certificates.''' 10 | 11 | return bool(getenv('FEDIPLAY_NO_CHECK_CERTIFICATE')) 12 | 13 | def play_command(): 14 | '''Returns the play command fediplay should use to play a file.''' 15 | 16 | return (getenv('FEDIPLAY_PLAY_COMMAND') or 17 | 'ffplay -v 0 -nostats -hide_banner -autoexit -nodisp {filename}') 18 | 19 | load_dotenv(find_dotenv()) 20 | -------------------------------------------------------------------------------- /fediplay/keyring.py: -------------------------------------------------------------------------------- 1 | '''Secret storage.''' 2 | 3 | import os 4 | path = os.path 5 | 6 | import appdirs 7 | import click 8 | from keyring import get_password, set_password 9 | 10 | from fediplay.dirs import DIRS 11 | 12 | 13 | SERVICE_NAME = 'fediplay' 14 | CREDENTIAL_CLIENT_ID = 'client_id' 15 | CREDENTIAL_CLIENT_SECRET = 'client_secret' 16 | CREDENTIAL_ACCESS_TOKEN = 'access_token' 17 | 18 | def build_username(instance, credential_kind): 19 | return credential_kind + '@' + instance 20 | 21 | def set_credential(instance, credential_kind, credential): 22 | set_password(SERVICE_NAME, build_username(instance, credential_kind), credential) 23 | 24 | def get_credential(instance, credential_kind): 25 | return get_password(SERVICE_NAME, build_username(instance, credential_kind)) 26 | 27 | def has_credential(instance, credential_kind): 28 | return get_credential(instance, credential_kind) is not None 29 | 30 | def migrate_client_credentials(instance): 31 | def migrate_and_unlink(filename): 32 | if path.exists(filename): 33 | click.echo('==> Migrating client credentials to keyring from ' + filename) 34 | 35 | with open(filename, 'r', encoding='utf-8') as infile: 36 | client_id = infile.readline().strip() 37 | client_secret = infile.readline().strip() 38 | 39 | set_credential(instance, CREDENTIAL_CLIENT_ID, client_id) 40 | set_credential(instance, CREDENTIAL_CLIENT_SECRET, client_secret) 41 | 42 | os.unlink(filename) 43 | 44 | migrate_and_unlink('clientcred.secret') 45 | migrate_and_unlink(path.join(DIRS.user_config_dir, instance + '.clientcred.secret')) 46 | 47 | def migrate_access_token(instance): 48 | def migrate_and_unlink(filename): 49 | if path.exists(filename): 50 | click.echo('==> Migrating access token to keyring from ' + filename) 51 | 52 | with open(filename, 'r', encoding='utf-8') as infile: 53 | access_token = infile.readline().strip() 54 | 55 | set_credential(instance, CREDENTIAL_ACCESS_TOKEN, access_token) 56 | 57 | os.unlink(filename) 58 | 59 | migrate_and_unlink('usercred.secret') 60 | migrate_and_unlink(path.join(DIRS.user_config_dir, instance + '.usercred.secret')) 61 | -------------------------------------------------------------------------------- /fediplay/mastodon.py: -------------------------------------------------------------------------------- 1 | '''Mastodon interface.''' 2 | 3 | LISTEN_TO_HASHTAG = 'fediplay' 4 | 5 | from os import umask 6 | 7 | import click 8 | from lxml.etree import HTML # pylint: disable=no-name-in-module 9 | import mastodon 10 | from youtube_dl.utils import DownloadError 11 | 12 | from fediplay.cli import options 13 | import fediplay.keyring as keyring 14 | from fediplay.queue import Queue 15 | 16 | Mastodon = mastodon.Mastodon 17 | 18 | 19 | def api_base_url(instance): 20 | '''Create an API base url from an instance name.''' 21 | 22 | return 'https://' + instance 23 | 24 | class StreamListener(mastodon.StreamListener): 25 | '''Listens to a Mastodon timeline and adds links the given Queue.''' 26 | 27 | def __init__(self, queue, instance, users): 28 | self.queue = queue 29 | self.instance = instance 30 | self.users = users 31 | 32 | if options['debug']: 33 | print('listener initialized with users={!r}'.format(self.users)) 34 | 35 | def on_update(self, status): 36 | if options['debug']: 37 | print('incoming status: acct={!r}'.format(status.account.acct)) 38 | 39 | if self.users and normalize_username(status.account.acct, self.instance) not in self.users: 40 | if options['debug']: 41 | print('skipping status due to username filtering') 42 | return 43 | 44 | tags = extract_tags(status) 45 | if options['debug']: 46 | print('expecting: {!r}, extracted tags: {!r}'.format(LISTEN_TO_HASHTAG, tags)) 47 | 48 | if LISTEN_TO_HASHTAG in tags: 49 | links = extract_links(status) 50 | if options['debug']: 51 | print('links: {!r}'.format(links)) 52 | 53 | for link in links: 54 | try: 55 | click.echo('==> Trying {}'.format(link)) 56 | self.queue.add(link) 57 | return 58 | except DownloadError: 59 | pass 60 | 61 | def register(instance): 62 | '''Register fediplay to a Mastodon server and save the client credentials.''' 63 | 64 | client_id, client_secret = Mastodon.create_app('fediplay', scopes=['read'], api_base_url=api_base_url(instance)) 65 | keyring.set_credential(instance, keyring.CREDENTIAL_CLIENT_ID, client_id) 66 | keyring.set_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET, client_secret) 67 | 68 | def build_client(instance, client_id, client_secret, access_token=None): 69 | '''Builds a Mastodon client.''' 70 | 71 | return Mastodon(api_base_url=api_base_url(instance), 72 | client_id=client_id, client_secret=client_secret, access_token=access_token) 73 | 74 | def get_auth_request_url(instance, client_id, client_secret): 75 | '''Gets an authorization request URL from a Mastodon instance.''' 76 | 77 | return build_client(instance, client_id, client_secret).auth_request_url(scopes=['read']) 78 | 79 | def login(instance, client_id, client_secret, grant_code): 80 | '''Log in to a Mastodon server and save the user credentials.''' 81 | 82 | client = build_client(instance, client_id, client_secret) 83 | access_token = client.log_in(code=grant_code, scopes=['read']) 84 | keyring.set_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN, access_token) 85 | 86 | def stream(instance, users, client_id, client_secret, access_token, cache_dir='.'): 87 | '''Stream statuses and add them to a queue.''' 88 | 89 | client = build_client(instance, client_id, client_secret, access_token) 90 | users = [normalize_username(user, instance) for user in users] 91 | listener = StreamListener(Queue(cache_dir), instance, users) 92 | 93 | existing_statuses = client.timeline_hashtag(LISTEN_TO_HASHTAG, limit=1) 94 | 95 | if options['debug']: 96 | print('existing_statuses: {!r}'.format(existing_statuses)) 97 | 98 | for status in existing_statuses: 99 | listener.on_update(status) 100 | 101 | click.echo('==> Streaming from {}'.format(instance)) 102 | client.stream_user(listener) 103 | 104 | def extract_tags(toot): 105 | '''Extract tags from a toot.''' 106 | 107 | return [tag['name'] for tag in toot['tags']] 108 | 109 | def normalize_username(user, instance): 110 | user = user.lstrip('@') 111 | parts = user.split('@') 112 | if options['debug']: 113 | print('parts: {!r}'.format(parts)) 114 | 115 | if len(parts) == 1 or parts[1] == instance: 116 | return parts[0] 117 | else: 118 | return user 119 | 120 | def link_is_internal(link): 121 | '''Determines if a link is internal to the Mastodon instance.''' 122 | 123 | classes = link.attrib.get('class', '').split(' ') 124 | 125 | if options['debug']: 126 | print('href: {!r}, classes: {!r}'.format(link.attrib['href'], classes)) 127 | 128 | if classes: 129 | return 'mention' in classes 130 | 131 | return False 132 | 133 | def extract_links(toot): 134 | '''Extract all external links from a toot.''' 135 | 136 | html = HTML(toot['content']) 137 | all_links = html.cssselect('a') 138 | return [link.attrib['href'] for link in all_links if not link_is_internal(link)] 139 | -------------------------------------------------------------------------------- /fediplay/queue.py: -------------------------------------------------------------------------------- 1 | '''The play queue.''' 2 | 3 | from os import path, listdir, makedirs, remove, utime 4 | from time import time, localtime 5 | import shlex 6 | from subprocess import run 7 | from threading import Thread, Lock 8 | 9 | import click 10 | from youtube_dl import YoutubeDL, utils 11 | 12 | from fediplay.cli import options 13 | import fediplay.env as env 14 | 15 | 16 | class Queue(object): 17 | '''The play queue.''' 18 | 19 | # pylint: disable=too-few-public-methods 20 | 21 | def __init__(self, cache_dir): 22 | self.lock = Lock() 23 | self.playing = False 24 | self.queue = [] 25 | self.cache_dir = cache_dir 26 | 27 | def add(self, url): 28 | '''Fetches the url and adds the resulting audio to the play queue.''' 29 | 30 | filenames = Getter(self.cache_dir).get(url) 31 | 32 | with self.lock: 33 | self.queue.extend(filenames) 34 | if not self.playing: 35 | self._play(self.queue.pop(0), self._play_finished) 36 | 37 | def _play(self, filename, cb_complete): 38 | self.playing = True 39 | 40 | def _run_thread(filename, cb_complete): 41 | play_command = build_play_command(filename) 42 | click.echo('==> Playing {} with {}'.format(filename, play_command)) 43 | run(play_command, shell=True) 44 | click.echo('==> Playback complete') 45 | cb_complete() 46 | 47 | thread = Thread(target=_run_thread, args=(filename, cb_complete)) 48 | thread.start() 49 | 50 | def _play_finished(self): 51 | with self.lock: 52 | self.playing = False 53 | if self.queue: 54 | self._play(self.queue.pop(0), self._play_finished) 55 | 56 | class Getter(object): 57 | '''Fetches music from a url.''' 58 | 59 | # pylint: disable=too-few-public-methods 60 | def __init__(self, cache_dir): 61 | self.filename = None 62 | self.filenames = [] 63 | self.cache_dir = cache_dir 64 | 65 | def _progress_hook(self, progress): 66 | if options['debug']: 67 | print('progress hook: status {!r}, filename {!r}'.format(progress['status'], progress['filename'])) 68 | 69 | if (progress['status'] in ('downloading', 'finished') and 70 | progress['filename'] not in self.filenames): 71 | self.filenames.append(progress['filename']) 72 | 73 | def get(self, url): 74 | '''Fetches music from the given url.''' 75 | 76 | '''deleting files here''' 77 | auto_delete_files(self.cache_dir) 78 | 79 | options = { 80 | 'format': 'mp3/mp4', 81 | 'nocheckcertificate': env.no_check_certificate(), 82 | 'outtmpl': path.join(self.cache_dir, utils.DEFAULT_OUTTMPL), 83 | 'progress_hooks': [self._progress_hook] 84 | } 85 | with YoutubeDL(options) as downloader: 86 | downloader.download([url]) 87 | 88 | for file in self.filenames: 89 | utime(file) 90 | 91 | return self.filenames 92 | 93 | def build_play_command(filename): 94 | '''Builds a play command for the given filename.''' 95 | 96 | escaped_filename = shlex.quote(filename) 97 | template = env.play_command() 98 | return template.format(filename=escaped_filename) 99 | 100 | def auto_delete_files(cache_dir): 101 | for the_file in listdir(cache_dir): 102 | file_path = path.join(cache_dir, the_file) 103 | if path.isfile(file_path): 104 | file_time = path.getmtime(file_path) 105 | if file_time + 604800 < time(): 106 | remove(file_path) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='fediplay', 5 | version='2.0', 6 | py_modules=['fediplay'], 7 | install_requires=[ 8 | 'appdirs', 9 | 'click', 10 | 'cssselect', 11 | 'lxml', 12 | 'Mastodon.py', 13 | 'python-dotenv', 14 | 'youtube-dl' 15 | ], 16 | entry_points={ 17 | 'console_scripts': [ 18 | 'fediplay = fediplay:cli' 19 | ] 20 | } 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /test_fediplay.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from fediplay.mastodon import extract_links 4 | from fediplay.queue import build_play_command 5 | 6 | 7 | def test_extract_links(): 8 | toot = { 9 | 'content': "

#nowplaying #fediplay Grimes ft. Janelle Mon\u00e1e - Venus Fly https://www.youtube.com/watch?v=eTLTXDHrgtw

" 10 | } 11 | urls = extract_links(toot) 12 | assert urls == ['https://www.youtube.com/watch?v=eTLTXDHrgtw'] 13 | 14 | def test_build_play_command_default(): 15 | environ.pop('FEDIPLAY_PLAY_COMMAND') 16 | play_command = build_play_command('Awesome Music.mp3') 17 | assert play_command == 'ffplay -v 0 -nostats -hide_banner -autoexit -nodisp \'Awesome Music.mp3\'' 18 | 19 | def test_build_play_command_specified(): 20 | environ.update(FEDIPLAY_PLAY_COMMAND='afplay {filename}') 21 | play_command = build_play_command('Awesome Music.mp3') 22 | assert play_command == 'afplay \'Awesome Music.mp3\'' 23 | --------------------------------------------------------------------------------