├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── demo.gif ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── watch.sh └── wickie ├── __init__.py ├── __main__.py ├── bot ├── __init__.py └── helpers.py ├── goodreads.py ├── notionutils ├── __init__.py ├── add.py └── prepare.py ├── omdb.py ├── settings.py └── utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403, F401 3 | max-line-length = 79 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env 3 | __pycache__ 4 | *.pyc 5 | *.code-workspace -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v1.2.3 9 | hooks: 10 | - id: flake8 11 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | black = "*" 8 | flake8 = "*" 9 | pep8 = "*" 10 | pylint = "*" 11 | pre-commit = "*" 12 | pipenv-to-requirements = "*" 13 | 14 | [packages] 15 | notion = "*" 16 | python-dotenv = "*" 17 | omdb = "*" 18 | beautifulsoup4 = "*" 19 | python-dateutil = "*" 20 | python-telegram-bot = "*" 21 | 22 | [requires] 23 | python_version = "3.7" 24 | 25 | [pipenv] 26 | allow_prereleases = true 27 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "59cb7f80abdb10e7e3016c8244a27656ba217bde82ec0c4eff50c41aca03ba79" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asn1crypto": { 20 | "hashes": [ 21 | "sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292", 22 | "sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f" 23 | ], 24 | "version": "==1.0.1" 25 | }, 26 | "beautifulsoup4": { 27 | "hashes": [ 28 | "sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169", 29 | "sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931", 30 | "sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57" 31 | ], 32 | "index": "pypi", 33 | "version": "==4.8.1" 34 | }, 35 | "bs4": { 36 | "hashes": [ 37 | "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" 38 | ], 39 | "version": "==0.0.1" 40 | }, 41 | "cached-property": { 42 | "hashes": [ 43 | "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", 44 | "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" 45 | ], 46 | "version": "==1.5.1" 47 | }, 48 | "certifi": { 49 | "hashes": [ 50 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 51 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 52 | ], 53 | "version": "==2019.9.11" 54 | }, 55 | "cffi": { 56 | "hashes": [ 57 | "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", 58 | "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", 59 | "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", 60 | "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", 61 | "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", 62 | "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", 63 | "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", 64 | "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", 65 | "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", 66 | "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", 67 | "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", 68 | "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", 69 | "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", 70 | "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", 71 | "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", 72 | "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", 73 | "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", 74 | "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", 75 | "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", 76 | "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", 77 | "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", 78 | "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", 79 | "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", 80 | "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", 81 | "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", 82 | "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", 83 | "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", 84 | "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" 85 | ], 86 | "version": "==1.12.3" 87 | }, 88 | "chardet": { 89 | "hashes": [ 90 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 91 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 92 | ], 93 | "version": "==3.0.4" 94 | }, 95 | "commonmark": { 96 | "hashes": [ 97 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", 98 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" 99 | ], 100 | "version": "==0.9.1" 101 | }, 102 | "cryptography": { 103 | "hashes": [ 104 | "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", 105 | "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", 106 | "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", 107 | "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", 108 | "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", 109 | "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", 110 | "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", 111 | "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", 112 | "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", 113 | "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", 114 | "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", 115 | "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", 116 | "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", 117 | "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", 118 | "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", 119 | "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" 120 | ], 121 | "version": "==2.7" 122 | }, 123 | "dictdiffer": { 124 | "hashes": [ 125 | "sha256:97cf4ef98ebc1acf737074aed41e379cf48ab5ff528c92109dfb8e2e619e6809", 126 | "sha256:b3ad476fc9cca60302b52c50e1839342d2092aeaba586d69cbf9249f87f52463" 127 | ], 128 | "version": "==0.8.0" 129 | }, 130 | "future": { 131 | "hashes": [ 132 | "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" 133 | ], 134 | "version": "==0.17.1" 135 | }, 136 | "idna": { 137 | "hashes": [ 138 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 139 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 140 | ], 141 | "version": "==2.8" 142 | }, 143 | "notion": { 144 | "hashes": [ 145 | "sha256:06c9709451ee5dde51681d6e12fb4549f769534c231f61d4f61a15ed3df238f0", 146 | "sha256:675197ee4b2d3759a2aff1cd560830c424257b789ca0a4ed2fb7a0579b305896" 147 | ], 148 | "index": "pypi", 149 | "version": "==0.0.24" 150 | }, 151 | "omdb": { 152 | "hashes": [ 153 | "sha256:2f44b25942f6ed4a2b51af3597417a8eaade30d2776cdbde9d9740cec8ff159c", 154 | "sha256:eeac363f1c2eb634e4ae898e227dedae810cce9b7ceba7e90c00c9219ee8f23b" 155 | ], 156 | "index": "pypi", 157 | "version": "==0.10.1" 158 | }, 159 | "pycparser": { 160 | "hashes": [ 161 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" 162 | ], 163 | "version": "==2.19" 164 | }, 165 | "python-dateutil": { 166 | "hashes": [ 167 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 168 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 169 | ], 170 | "index": "pypi", 171 | "version": "==2.8.0" 172 | }, 173 | "python-dotenv": { 174 | "hashes": [ 175 | "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", 176 | "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" 177 | ], 178 | "index": "pypi", 179 | "version": "==0.10.3" 180 | }, 181 | "python-slugify": { 182 | "hashes": [ 183 | "sha256:575d03256a132fc1efb4c52966c6eb11c57a13b071618f0b26076057a23f6937" 184 | ], 185 | "version": "==3.0.4" 186 | }, 187 | "python-telegram-bot": { 188 | "hashes": [ 189 | "sha256:43a67c1f6da444e1baabb960467d1524f81c71166b1235c5bf6d225332c01c3e", 190 | "sha256:c7c56ea3bf9874e39397633a35478b9bed44ba35db49cfd3ce97d331edef82a9" 191 | ], 192 | "index": "pypi", 193 | "version": "==12.1.1" 194 | }, 195 | "pytz": { 196 | "hashes": [ 197 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 198 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 199 | ], 200 | "version": "==2019.3" 201 | }, 202 | "requests": { 203 | "hashes": [ 204 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 205 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 206 | ], 207 | "version": "==2.22.0" 208 | }, 209 | "six": { 210 | "hashes": [ 211 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 212 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 213 | ], 214 | "version": "==1.12.0" 215 | }, 216 | "soupsieve": { 217 | "hashes": [ 218 | "sha256:605f89ad5fdbfefe30cdc293303665eff2d188865d4dbe4eb510bba1edfbfce3", 219 | "sha256:b91d676b330a0ebd5b21719cb6e9b57c57d433671f65b9c28dd3461d9a1ed0b6" 220 | ], 221 | "version": "==1.9.4" 222 | }, 223 | "text-unidecode": { 224 | "hashes": [ 225 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 226 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 227 | ], 228 | "version": "==1.3" 229 | }, 230 | "tornado": { 231 | "hashes": [ 232 | "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", 233 | "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", 234 | "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", 235 | "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", 236 | "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", 237 | "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", 238 | "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5" 239 | ], 240 | "version": "==6.0.3" 241 | }, 242 | "tzlocal": { 243 | "hashes": [ 244 | "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048", 245 | "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590" 246 | ], 247 | "version": "==2.0.0" 248 | }, 249 | "urllib3": { 250 | "hashes": [ 251 | "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", 252 | "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" 253 | ], 254 | "version": "==1.25.6" 255 | } 256 | }, 257 | "develop": { 258 | "appdirs": { 259 | "hashes": [ 260 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 261 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 262 | ], 263 | "version": "==1.4.3" 264 | }, 265 | "aspy.yaml": { 266 | "hashes": [ 267 | "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", 268 | "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" 269 | ], 270 | "version": "==1.3.0" 271 | }, 272 | "astroid": { 273 | "hashes": [ 274 | "sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", 275 | "sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6" 276 | ], 277 | "version": "==2.3.1" 278 | }, 279 | "attrs": { 280 | "hashes": [ 281 | "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", 282 | "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" 283 | ], 284 | "version": "==19.2.0" 285 | }, 286 | "black": { 287 | "hashes": [ 288 | "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", 289 | "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" 290 | ], 291 | "index": "pypi", 292 | "version": "==19.3b0" 293 | }, 294 | "certifi": { 295 | "hashes": [ 296 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 297 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 298 | ], 299 | "version": "==2019.9.11" 300 | }, 301 | "cfgv": { 302 | "hashes": [ 303 | "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", 304 | "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" 305 | ], 306 | "version": "==2.0.1" 307 | }, 308 | "click": { 309 | "hashes": [ 310 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 311 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 312 | ], 313 | "version": "==7.0" 314 | }, 315 | "entrypoints": { 316 | "hashes": [ 317 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 318 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 319 | ], 320 | "version": "==0.3" 321 | }, 322 | "flake8": { 323 | "hashes": [ 324 | "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", 325 | "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" 326 | ], 327 | "index": "pypi", 328 | "version": "==3.7.8" 329 | }, 330 | "identify": { 331 | "hashes": [ 332 | "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", 333 | "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" 334 | ], 335 | "version": "==1.4.7" 336 | }, 337 | "importlib-metadata": { 338 | "hashes": [ 339 | "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", 340 | "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" 341 | ], 342 | "version": "==0.23" 343 | }, 344 | "isort": { 345 | "hashes": [ 346 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 347 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 348 | ], 349 | "version": "==4.3.21" 350 | }, 351 | "lazy-object-proxy": { 352 | "hashes": [ 353 | "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", 354 | "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", 355 | "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", 356 | "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", 357 | "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", 358 | "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", 359 | "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", 360 | "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", 361 | "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", 362 | "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", 363 | "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", 364 | "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", 365 | "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", 366 | "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", 367 | "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", 368 | "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", 369 | "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", 370 | "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1" 371 | ], 372 | "version": "==1.4.2" 373 | }, 374 | "mccabe": { 375 | "hashes": [ 376 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 377 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 378 | ], 379 | "version": "==0.6.1" 380 | }, 381 | "more-itertools": { 382 | "hashes": [ 383 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", 384 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" 385 | ], 386 | "version": "==7.2.0" 387 | }, 388 | "nodeenv": { 389 | "hashes": [ 390 | "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" 391 | ], 392 | "version": "==1.3.3" 393 | }, 394 | "pbr": { 395 | "hashes": [ 396 | "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", 397 | "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" 398 | ], 399 | "version": "==5.4.3" 400 | }, 401 | "pep8": { 402 | "hashes": [ 403 | "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee", 404 | "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374" 405 | ], 406 | "index": "pypi", 407 | "version": "==1.7.1" 408 | }, 409 | "pipenv": { 410 | "hashes": [ 411 | "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", 412 | "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", 413 | "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" 414 | ], 415 | "version": "==2018.11.26" 416 | }, 417 | "pipenv-to-requirements": { 418 | "hashes": [ 419 | "sha256:115390158232f53983f1d989f922d890adbdaaaa95c8b2357a77b9f5fe647862", 420 | "sha256:5b7349e76d2c511e8b4a723495311b310e4e33bb716437b30cc18ecb0b0b5e29" 421 | ], 422 | "index": "pypi", 423 | "version": "==0.8.2" 424 | }, 425 | "pre-commit": { 426 | "hashes": [ 427 | "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", 428 | "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" 429 | ], 430 | "index": "pypi", 431 | "version": "==1.18.3" 432 | }, 433 | "pycodestyle": { 434 | "hashes": [ 435 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 436 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 437 | ], 438 | "version": "==2.5.0" 439 | }, 440 | "pyflakes": { 441 | "hashes": [ 442 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 443 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 444 | ], 445 | "version": "==2.1.1" 446 | }, 447 | "pylint": { 448 | "hashes": [ 449 | "sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", 450 | "sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2" 451 | ], 452 | "index": "pypi", 453 | "version": "==2.4.2" 454 | }, 455 | "pyyaml": { 456 | "hashes": [ 457 | "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", 458 | "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", 459 | "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", 460 | "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", 461 | "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", 462 | "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", 463 | "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", 464 | "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", 465 | "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", 466 | "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", 467 | "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", 468 | "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", 469 | "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" 470 | ], 471 | "version": "==5.1.2" 472 | }, 473 | "six": { 474 | "hashes": [ 475 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 476 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 477 | ], 478 | "version": "==1.12.0" 479 | }, 480 | "toml": { 481 | "hashes": [ 482 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 483 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 484 | ], 485 | "version": "==0.10.0" 486 | }, 487 | "typed-ast": { 488 | "hashes": [ 489 | "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", 490 | "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", 491 | "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", 492 | "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", 493 | "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", 494 | "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", 495 | "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", 496 | "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", 497 | "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", 498 | "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", 499 | "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", 500 | "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", 501 | "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", 502 | "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", 503 | "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" 504 | ], 505 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 506 | "version": "==1.4.0" 507 | }, 508 | "virtualenv": { 509 | "hashes": [ 510 | "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", 511 | "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" 512 | ], 513 | "version": "==16.7.5" 514 | }, 515 | "virtualenv-clone": { 516 | "hashes": [ 517 | "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", 518 | "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" 519 | ], 520 | "version": "==0.5.3" 521 | }, 522 | "wrapt": { 523 | "hashes": [ 524 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 525 | ], 526 | "version": "==1.11.2" 527 | }, 528 | "zipp": { 529 | "hashes": [ 530 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 531 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 532 | ], 533 | "version": "==0.6.0" 534 | } 535 | } 536 | } 537 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python -m wickie -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wickie 2 | 3 | ![](demo.gif) 4 | 5 | ## How to Start 6 | 7 | ```bash 8 | $ pipenv install 9 | $ pipenv shell 10 | $ pre-commit install 11 | # Start nodemon watcher: 12 | $ ./watch.sh 13 | # or just run it once: 14 | $ python -m wickie 15 | ``` 16 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benknab/wickie/173fbfd4660ca5ac9107a67f3a87a588c58de000/demo.gif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | include = '\.pyi?$' 4 | exclude = ''' 5 | /( 6 | \.git 7 | | \.hg 8 | | \.mypy_cache 9 | | \.tox 10 | | \.venv 11 | | _build 12 | | buck-out 13 | | build 14 | | dist 15 | )/ 16 | ''' -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This requirements file has been automatically generated from `Pipfile` with 3 | # `pipenv-to-requirements` 4 | # 5 | # 6 | # This has been done to maintain backward compatibility with tools and services 7 | # that do not support `Pipfile` yet. 8 | # 9 | # Do NOT edit it directly, use `pipenv install [-d]` to modify `Pipfile` and 10 | # `Pipfile.lock` and then regenerate `requirements*.txt`. 11 | ################################################################################ 12 | 13 | appdirs==1.4.3 14 | aspy.yaml==1.3.0 15 | astroid==2.3.1 16 | attrs==19.2.0 17 | black==19.3b0 18 | certifi==2019.9.11 19 | cfgv==2.0.1 20 | click==7.0 21 | entrypoints==0.3 22 | flake8==3.7.8 23 | identify==1.4.7 24 | importlib-metadata==0.23 25 | isort==4.3.21 26 | lazy-object-proxy==1.4.2 27 | mccabe==0.6.1 28 | more-itertools==7.2.0 29 | nodeenv==1.3.3 30 | pbr==5.4.3 31 | pep8==1.7.1 32 | pipenv-to-requirements==0.8.2 33 | pipenv==2018.11.26 34 | pre-commit==1.18.3 35 | pycodestyle==2.5.0 36 | pyflakes==2.1.1 37 | pylint==2.4.2 38 | pyyaml==5.1.2 39 | six==1.12.0 40 | toml==0.10.0 41 | typed-ast==1.4.0 ; implementation_name == 'cpython' and python_version < '3.8' 42 | virtualenv-clone==0.5.3 43 | virtualenv==16.7.5 44 | wrapt==1.11.2 45 | zipp==0.6.0 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This requirements file has been automatically generated from `Pipfile` with 3 | # `pipenv-to-requirements` 4 | # 5 | # 6 | # This has been done to maintain backward compatibility with tools and services 7 | # that do not support `Pipfile` yet. 8 | # 9 | # Do NOT edit it directly, use `pipenv install [-d]` to modify `Pipfile` and 10 | # `Pipfile.lock` and then regenerate `requirements*.txt`. 11 | ################################################################################ 12 | 13 | asn1crypto==1.0.1 14 | beautifulsoup4==4.8.1 15 | bs4==0.0.1 16 | cached-property==1.5.1 17 | certifi==2019.9.11 18 | cffi==1.12.3 19 | chardet==3.0.4 20 | commonmark==0.9.1 21 | cryptography==2.7 22 | dictdiffer==0.8.0 23 | future==0.17.1 24 | idna==2.8 25 | notion==0.0.24 26 | omdb==0.10.1 27 | pycparser==2.19 28 | python-dateutil==2.8.0 29 | python-dotenv==0.10.3 30 | python-slugify==3.0.4 31 | python-telegram-bot==12.1.1 32 | pytz==2019.3 33 | requests==2.22.0 34 | six==1.12.0 35 | soupsieve==1.9.4 36 | text-unidecode==1.3 37 | tornado==6.0.3 38 | tzlocal==2.0.0 39 | urllib3==1.25.6 40 | -------------------------------------------------------------------------------- /watch.sh: -------------------------------------------------------------------------------- 1 | # If nodemon is globally installed 2 | nodemon -e py --exec "python -m" wickie -------------------------------------------------------------------------------- /wickie/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benknab/wickie/173fbfd4660ca5ac9107a67f3a87a588c58de000/wickie/__init__.py -------------------------------------------------------------------------------- /wickie/__main__.py: -------------------------------------------------------------------------------- 1 | import wickie.bot as bot 2 | 3 | 4 | def main(): 5 | bot.launch() 6 | 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /wickie/bot/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | import traceback 4 | 5 | from telegram import ReplyKeyboardRemove 6 | from telegram.ext import ( 7 | Updater, 8 | CommandHandler, 9 | MessageHandler, 10 | Filters, 11 | ConversationHandler, 12 | ) 13 | 14 | from wickie.bot.helpers import ( 15 | logger, 16 | restricted, 17 | send_typing_action, 18 | create_confirmation_keyboard, 19 | CONFIRMATION_STATE, 20 | YES, 21 | NO, 22 | NEW_PAGE_KEY, 23 | ) 24 | from wickie.utils import prettier 25 | from wickie.omdb import client as omdb_client 26 | from wickie.settings import TELEGRAM_BOT_TOKEN 27 | import wickie.notionutils.prepare as prepare_for_notion 28 | from wickie.notionutils.add import add as add_to_notion 29 | import wickie.omdb as omdb 30 | import wickie.goodreads as goodreads 31 | 32 | 33 | @restricted 34 | def handle_start(update, context): 35 | """Send a message when the command /start is issued.""" 36 | update.message.reply_text("Hi!") 37 | 38 | 39 | @restricted 40 | def handle_help(update, context): 41 | """Send a message when the command /help is issued.""" 42 | update.message.reply_text("Help!") 43 | 44 | 45 | def handle_potential_page(page, update, context): 46 | """Handle common operations for IMDb and Goodreads handlers.""" 47 | update.message.reply_text( 48 | prettier(page), reply_markup=create_confirmation_keyboard() 49 | ) 50 | update.message.reply_text("Looks good? 🔍") 51 | context.user_data[NEW_PAGE_KEY] = page 52 | return CONFIRMATION_STATE 53 | 54 | 55 | @restricted 56 | @send_typing_action 57 | def handle_imdb(update, context): 58 | """Handle IMDb URL.""" 59 | imdb_id = omdb.extract_id(update.message.text) 60 | omdb_response = omdb_client.get(imdbid=imdb_id) 61 | 62 | page = None 63 | if omdb_response["type"] == "series": 64 | page = prepare_for_notion.series(omdb_response) 65 | if omdb_response["type"] == "movie": 66 | page = prepare_for_notion.film(omdb_response) 67 | 68 | return handle_potential_page(page, update, context) 69 | 70 | 71 | @restricted 72 | @send_typing_action 73 | def handle_goodreads(update, context): 74 | """Handle Goodreads URL.""" 75 | book = prepare_for_notion.book(goodreads.get(update.message.text)) 76 | return handle_potential_page(book, update, context) 77 | 78 | 79 | @restricted 80 | @send_typing_action 81 | def handle_accept(update, context): 82 | update.message.reply_text("Great! Creating a Notion page... 🔦") 83 | try: 84 | url = add_to_notion(context.user_data[NEW_PAGE_KEY]) 85 | update.message.reply_text(f"Check out your new page at: {url} 👀") 86 | except Exception: # TODO Catch more specific exception 87 | update.message.reply_text("Something has went wrong! 🤦‍♀️") 88 | traceback.print_exc() 89 | context.user_data.clear() 90 | return ConversationHandler.END 91 | 92 | 93 | @restricted 94 | def handle_reject(update, context): 95 | update.message.reply_text("Sorry. 😔") 96 | context.user_data.clear() 97 | return ConversationHandler.END 98 | 99 | 100 | @restricted 101 | def handle_unknown(update, context): 102 | update.message.reply_text("Unknown Command") 103 | 104 | 105 | @restricted 106 | def handle_error(update, context): 107 | """Log Errors caused by Updates.""" 108 | logger.warning(f'Update "{update}" caused error "{context.error}"') 109 | logger.warning(f"Trace: {traceback.format_exc()}") 110 | 111 | 112 | def launch(): 113 | """Start the bot.""" 114 | updater = Updater(TELEGRAM_BOT_TOKEN, use_context=True) 115 | 116 | dp = updater.dispatcher 117 | 118 | dp.add_handler(CommandHandler("start", handle_start)) 119 | dp.add_handler(CommandHandler("help", handle_help)) 120 | 121 | ch = ConversationHandler( 122 | entry_points=[ 123 | MessageHandler(Filters.regex(omdb.r_imdb_url), handle_imdb), 124 | MessageHandler( 125 | Filters.regex(goodreads.r_goodreads_url), handle_goodreads 126 | ), 127 | ], 128 | states={ 129 | CONFIRMATION_STATE: [ 130 | MessageHandler(Filters.regex(YES), handle_accept), 131 | MessageHandler(Filters.regex(NO), handle_reject), 132 | ] 133 | }, 134 | fallbacks=[MessageHandler(Filters.text, handle_unknown)], 135 | ) 136 | dp.add_handler(ch) 137 | 138 | dp.add_handler(MessageHandler(Filters.text, handle_unknown)) 139 | dp.add_error_handler(handle_error) 140 | 141 | updater.start_polling() 142 | 143 | updater.idle() 144 | -------------------------------------------------------------------------------- /wickie/bot/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import wraps 3 | 4 | from telegram import ReplyKeyboardMarkup, ChatAction 5 | 6 | from wickie.settings import TELEGRAM_USER_ID 7 | 8 | 9 | CONFIRMATION_STATE = 0 10 | 11 | YES = "Yes 👍" 12 | NO = "No 😢" 13 | 14 | NEW_PAGE_KEY = 0 15 | 16 | logging.basicConfig( 17 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 18 | level=logging.INFO, 19 | ) 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def restricted(func): 25 | """Restrict usage of func to authorized users only""" 26 | 27 | @wraps(func) 28 | def wrapped(update, context, *args, **kwargs): 29 | user_id = update.effective_user.id 30 | if user_id != TELEGRAM_USER_ID: 31 | logger.warning( 32 | "Unauthorized access denied for {}.".format(user_id) 33 | ) 34 | return update.message.reply_text("Unauthorized user.") 35 | return func(update, context, *args, **kwargs) 36 | 37 | return wrapped 38 | 39 | 40 | def send_typing_action(func): 41 | """Sends typing action while processing func command.""" 42 | 43 | @wraps(func) 44 | def command_func(update, context, *args, **kwargs): 45 | context.bot.send_chat_action( 46 | chat_id=update.effective_message.chat_id, action=ChatAction.TYPING 47 | ) 48 | return func(update, context, *args, **kwargs) 49 | 50 | return command_func 51 | 52 | 53 | def create_confirmation_keyboard(): 54 | return ReplyKeyboardMarkup([[YES, NO]], one_time_keyboard=True) 55 | -------------------------------------------------------------------------------- /wickie/goodreads.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | from bs4 import BeautifulSoup 5 | from dateutil.parser import parse as parse_date 6 | 7 | 8 | r_goodreads_url = ( 9 | r"https?://(w{3}\.)?goodreads\.com/book/show/([0-9]+).(.*)(\?(.*))?" 10 | ) 11 | 12 | 13 | def _scrape_title(soup): 14 | meta_title = soup.find("meta", attrs={"property": "og:title"})["content"] 15 | return re.sub(r" by [a-zA-Z ]*", "", meta_title.strip()) 16 | 17 | 18 | def _extract_author_names(author_name_containers): 19 | authors_without_extra_roles = [ 20 | author.a.span.string 21 | for author in author_name_containers 22 | if author.find(class_="role") is None 23 | ] 24 | return authors_without_extra_roles 25 | 26 | 27 | def _scrape_authors(soup): 28 | authors = soup.find(id="bookAuthors") 29 | author_name_containers = _extract_author_names( 30 | authors.find_all(class_="authorName__container") 31 | ) 32 | return author_name_containers 33 | 34 | 35 | def _extract_publication_date(publication_details): 36 | """ 37 | Example `parts` array: 38 | ['Published', 'June 3rd 2003', 'by Modern Library', '(first published 1927)'] 39 | the last item doesn't necessarily exist 40 | """ 41 | parts = [ 42 | d.strip() 43 | for d in publication_details.get_text().split("\n") 44 | if d.strip() != "" 45 | ] 46 | 47 | # e.g.: June 3rd 2003 48 | r_spd = r"([A-Z][a-z]*) ([0-9]*[a-z]*) ([0-9]*)" 49 | specific_publication_date = next( 50 | (p for p in parts if re.fullmatch(r_spd, p) is not None), None 51 | ) 52 | 53 | # e.g.: (first published 1927) 54 | r_fpd = r"(\(first published )(.*)(\))" 55 | first_publication_date = next( 56 | ( 57 | p.strip("(first published")[:-1] 58 | for p in parts 59 | if re.fullmatch(r_fpd, p) is not None 60 | ), 61 | None, 62 | ) 63 | 64 | return ( 65 | first_publication_date 66 | if first_publication_date is not None 67 | else specific_publication_date 68 | ) 69 | 70 | 71 | def _scrape_publication_date(soup): 72 | details = soup.find(id="details").find_all(class_="row") 73 | if len(details) > 0: 74 | # Find the publication details row after page number row 75 | publication_details = details[1] 76 | publication_date = _extract_publication_date(publication_details) 77 | return publication_date 78 | return "" 79 | 80 | 81 | def _scrape_genres(soup): 82 | # e.g.: ['Classics', '394 users'] 83 | genres = [ 84 | g.string 85 | for g in soup.find_all(class_="bookPageGenreLink") 86 | # Remove how many users marked the genre 87 | if re.fullmatch(r"[,0-9]* users?", g.string) is None 88 | ] 89 | return list(set(genres)) 90 | 91 | 92 | def _scrape_cover_image(soup): 93 | return soup.find(id="coverImage")["src"] 94 | 95 | 96 | def _scrape_book(soup): 97 | return { 98 | "title": _scrape_title(soup), 99 | "authors": _scrape_authors(soup), 100 | "publication_date": _scrape_publication_date(soup), 101 | "genres": _scrape_genres(soup), 102 | "cover_image": _scrape_cover_image(soup), 103 | } 104 | 105 | 106 | def get(url): 107 | r = requests.get(url) 108 | soup = BeautifulSoup(r.content, "html.parser") 109 | return {"url": url, **_scrape_book(soup)} 110 | -------------------------------------------------------------------------------- /wickie/notionutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benknab/wickie/173fbfd4660ca5ac9107a67f3a87a588c58de000/wickie/notionutils/__init__.py -------------------------------------------------------------------------------- /wickie/notionutils/add.py: -------------------------------------------------------------------------------- 1 | import os 2 | from uuid import uuid1 3 | from random import choice 4 | from pprint import pprint 5 | 6 | from notion.client import NotionClient 7 | from notion.block import ImageBlock 8 | 9 | from wickie.settings import NOTION_PAGE, NOTION_TOKEN 10 | 11 | notion_client = NotionClient(token_v2=NOTION_TOKEN) 12 | collection_view = notion_client.get_collection_view( 13 | NOTION_PAGE, force_refresh=True 14 | ) 15 | collection = collection_view.collection 16 | 17 | colors = [ 18 | "default", 19 | "gray", 20 | "brown", 21 | "orange", 22 | "yellow", 23 | "green", 24 | "blue", 25 | "purple", 26 | "pink", 27 | "red", 28 | ] 29 | 30 | 31 | def _find_prop_schema(schema, prop): 32 | return next((v for k, v in schema.items() if v["name"] == prop), None) 33 | 34 | 35 | def _add_new_multi_select_value(schema, prop, value, color=None): 36 | if color is None: 37 | color = choice(colors) 38 | 39 | prop_schema = _find_prop_schema(schema, prop) 40 | if not prop_schema: 41 | raise ValueError( 42 | f'"{prop}" property does not exist on the collection!' 43 | ) 44 | if prop_schema["type"] != "multi_select": 45 | raise ValueError(f'"{prop}" is not a multi select property!') 46 | 47 | dupe = next( 48 | (o for o in prop_schema["options"] if o["value"] == value), None 49 | ) 50 | if dupe: 51 | raise ValueError(f'"{value}" already exists in the schema!') 52 | 53 | prop_schema["options"].append( 54 | {"id": str(uuid1()), "value": value, "color": color} 55 | ) 56 | try: 57 | collection.set("schema", schema) 58 | except (RecursionError, UnicodeEncodeError): 59 | # Catch `RecursionError` and `UnicodeEncodeError` 60 | # in `notion-py/store.py/run_local_operation`, 61 | # because I've no idea why does it raise an error. 62 | # The schema is correctly updated on remote. 63 | pass 64 | 65 | 66 | def _set_multi_select_property(page, schema, prop, new_values): 67 | new_values_set = set(new_values) 68 | current_options_set = set( 69 | [o["value"] for o in _find_prop_schema(schema, prop)["options"]] 70 | ) 71 | intersection = new_values_set.intersection(current_options_set) 72 | if len(new_values_set) > len(intersection): 73 | difference = [v for v in new_values_set if v not in intersection] 74 | for d in difference: 75 | _add_new_multi_select_value(schema, prop, d) 76 | page.set_property(prop, new_values) 77 | 78 | 79 | def _set_content_cover(block, src): 80 | image = block.children.add_new(ImageBlock) 81 | image.set_source_url(src) 82 | 83 | 84 | def add(prepared_dict): 85 | schema = collection.get("schema") 86 | new_block = collection_view.collection.add_row() 87 | 88 | if "Cover" in prepared_dict: 89 | _set_content_cover(new_block, prepared_dict["Cover"]) 90 | prepared_dict.pop("Cover") 91 | 92 | for prop, value in prepared_dict.items(): 93 | if isinstance(value, list): 94 | _set_multi_select_property(new_block, schema, prop, value) 95 | else: 96 | new_block.set_property(prop, value) 97 | 98 | return new_block.get_browseable_url() 99 | -------------------------------------------------------------------------------- /wickie/notionutils/prepare.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from enum import Enum 4 | 5 | from dateutil.parser import parse as parse_date 6 | 7 | 8 | props = { 9 | "name": "Name", 10 | "cover": "Cover", 11 | "status": "Status", 12 | "category": "Category", 13 | "link": "Link", 14 | "authors": "Authors", 15 | "creators": "Creators", 16 | "directors": "Directors", 17 | "writers": "Writers", 18 | "actors": "Actors", 19 | "why": "Why", 20 | "tags": "Tags", 21 | "genres": "Genres", 22 | "date": "Date", 23 | "year": "Year", 24 | "publisher": "Publisher", 25 | "start_date": "Start Date", 26 | "finish_date": "Finish Date", 27 | "score": "Score", 28 | "description": "Description", 29 | } 30 | 31 | 32 | def _publication_date(publication_date): 33 | try: 34 | year = parse_date(publication_date).year 35 | return year 36 | except ValueError: 37 | return int(publication_date) 38 | 39 | 40 | def _genres(genres): 41 | return [g.strip() for g in genres.split(",")] 42 | 43 | 44 | def _celebrity(celebrity): 45 | return re.sub(r"\(.*\)", "", celebrity).strip() 46 | 47 | 48 | def _celebrities(cast): 49 | return list(set([_celebrity(c) for c in cast.split(",")])) 50 | 51 | 52 | def book(scraped_book): 53 | name = (props["name"], scraped_book["title"]) 54 | category = (props["category"], "Book") 55 | status = (props["status"], "Not Started") 56 | link = (props["link"], scraped_book["url"]) 57 | year = (props["year"], _publication_date(scraped_book["publication_date"])) 58 | cover = (props["cover"], scraped_book["cover_image"]) 59 | genres = (props["genres"], scraped_book["genres"]) 60 | authors = (props["authors"], scraped_book["authors"]) 61 | return dict([name, category, status, link, year, cover, genres, authors]) 62 | 63 | 64 | def _omdb_common(omdb_response): 65 | name = (props["name"], omdb_response["title"]) 66 | status = (props["status"], "Not Started") 67 | link = ( 68 | props["link"], 69 | f'https://imdb.com/title/{omdb_response["imdb_id"]}', 70 | ) 71 | date = (props["date"], parse_date(omdb_response["released"])) 72 | year = (props["year"], int(date[1].year)) 73 | cover = (props["cover"], omdb_response["poster"]) 74 | genres = (props["genres"], _genres(omdb_response["genre"])) 75 | actors = (props["actors"], _celebrities(omdb_response["actors"])) 76 | description = (props["description"], omdb_response["plot"]) 77 | return [name, status, link, year, cover, genres, date, actors, description] 78 | 79 | 80 | def film(omdb_film): 81 | category = (props["category"], "Film") 82 | directors = (props["directors"], _celebrities(omdb_film["director"])) 83 | writers = (props["writers"], _celebrities(omdb_film["writer"])) 84 | return dict(_omdb_common(omdb_film) + [category, directors, writers]) 85 | 86 | 87 | def series(omdb_series): 88 | category = (props["category"], "Series") 89 | creators = (props["creators"], _celebrities(omdb_series["writer"])) 90 | return dict(_omdb_common(omdb_series) + [category, creators]) 91 | -------------------------------------------------------------------------------- /wickie/omdb.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from omdb import OMDBClient 4 | 5 | from wickie.settings import OMDB_API_KEY 6 | 7 | r_imdb_url = r"https?://(w{3}.)?imdb.com/title/(tt[0-9]*)/?" 8 | client = OMDBClient(apikey=OMDB_API_KEY) 9 | 10 | 11 | def extract_id(url): 12 | match = re.match(r_imdb_url, url) 13 | if match: 14 | return match.groups()[-1] 15 | return None 16 | -------------------------------------------------------------------------------- /wickie/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") 8 | TELEGRAM_USER_ID = int(os.getenv("TELEGRAM_USER_ID")) 9 | 10 | OMDB_API_KEY = os.getenv("OMDB_API_KEY") 11 | 12 | NOTION_TOKEN = os.getenv("NOTION_TOKEN") 13 | NOTION_PAGE = os.getenv("NOTION_PAGE") 14 | -------------------------------------------------------------------------------- /wickie/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def format_date(datetime_): 5 | return datetime_.strftime("%Y/%m/%d") 6 | 7 | 8 | def join(list_, by=","): 9 | return by.join(list_) 10 | 11 | 12 | def prettier(dict_): 13 | string = "" 14 | for key, value in dict_.items(): 15 | prettier_value = value 16 | if isinstance(value, int): 17 | prettier_value = str(value) 18 | if isinstance(value, list): 19 | prettier_value = join(value) 20 | if isinstance(value, datetime): 21 | prettier_value = format_date(value) 22 | string = "{}{}: {}\n".format(string, key, prettier_value) 23 | return string 24 | --------------------------------------------------------------------------------