æè¿ã§ã¯ããã¬ã³ãããæ°ããã·ãªãŒãºãããŠã³ããŒãããããã»ã¹ãèªååããæ¹æ³ã«é¢ããHabrã«é¢ããèšäºã2ã€ãããŸããã äž¡æ¹ã®èšäºã®èè
ãã¢ããªã±ãŒã·ã§ã³ãå
±æããŸããã 1幎éãç§ãã¡ãåæ§ã®ã¢ããªã±ãŒã·ã§ã³ãéçºããŠããŸããããç§ãã¡ã®å°ããªãçŸãããããžã§ã¯ãMonitorrentã«ã€ããŠhabrasocietyã«è©±ãæãæ¥ãããã§ãã

Webã¢ããªã±ãŒã·ã§ã³ã¯Python 2ã§èšè¿°ãããŠããŸãïŒPython 3ããµããŒãããŠããŸãïŒã ç£èŠçšã®æ°ãããã¬ã³ãã远å ããæ°ããã·ãªãŒãºãèªåçã«ããŠã³ããŒãããŠããã¬ã³ãã¯ã©ã€ã¢ã³ãã«è¿œå ã§ããŸãã
æšå¹Žæ«ããç¶ç¶çã«äœ¿çšããŠããã2016幎5æ1æ¥ã«æåã®ãªãªãŒã¹ããŒãžã§ã³ããªãªãŒã¹ããŸãããããã¯ãDockerã³ã³ããã®cubietruckã§äžå
·åããªãããŸã å転ããŠããŸãã
å
éšã§ã®åäœã®è©³çްã«ã€ããŠã¯ã ç«ã
ç§ã¯ä»äºããå®¶ã«åž°ããå€é£ã«åº§ã£ãŠãKodiãéããŠãç§ã®ãæ°ã«å
¥ãã®ã·ãªãŒãºã®æ°é®®ãªã·ãªãŒãºãéžæãããããèŠãŸãã ãã¬ã³ããã©ãã«ãŒã§æ€çŽ¢ããããã®åªåãããã«ãããŠã³ããŒãããã®ãåŸ
ã€æéãç¡é§ã«ããããšããããŸããã
ãã®èªååã«ã¯å€ãã®ãœãªã¥ãŒã·ã§ã³ããããŸãã æåã«ãChromeçšã®ãã©ã°ã€ã³ã䜿çšããŸãããããã¯rutrackerã®å€æŽãç£èŠãããã¬ã³ããæåã§ããŠã³ããŒãããRDCãä»ããŠuTorrentã«è¿œå ããåŸã§Webã¢ããªã±ãŒã·ã§ã³ãä»ããŠè¿œå ããŸããã
ãã¬ã³ãã¢ãã¿ãŒ
ããããTorrentMonitorãçºèŠããåŸããã¹ãŠããã£ãšã·ã³ãã«ã«ãªããŸããã 圌ã¯1幎以äžã«ãŒã¿ãŒã§åããŠããŸããã 圌ã«ã¯ããã€ãã®ãã«ãªã¯ãšã¹ããããããŸããã ãã®ã¢ããªã±ãŒã·ã§ã³ã«ã€ããŠã¯ã圌ã®èè
ããããã«é¢ãã2ã€ã®çŽ æŽãããèšäºããããŸããïŒ 1ã2 ïŒã èè
ã«æè¬ããŸãã
TorrentMonitorã¯çŽ æŽãããã§ãããç§ã«ã¯åžžã«1ã€ã®åé¡ããããŸããã ãµã€ãºããŒãã®ãã¡ã€ã«ãããŠã³ããŒããããããšããããŸããã ç§ã¯èªåã®æã§ããŒã¿ããŒã¹ã«ã¢ã¯ã»ã¹ãããã®ã·ãªãŒãºããŸã ããŠã³ããŒããããŠããªããšããæ
å ±ãä¿®æ£ããå¿
èŠããããŸããïŒãã®åé¡ã¯æ¢ã«ä¿®æ£ãããŠããããã§ãïŒã åœæã圌ã¯ããŠã³ããŒããããã¬ã³ãããã¬ã³ãã¯ã©ã€ã¢ã³ãèªèº«ã«è¿œå ã§ããŸããã§ããïŒç§ã®å Žåã¯TransmissionïŒã ããã§ãã¹ãŠé 調ã§ãã
ãã¬ãã¯ã¹ã²ãã
ç§ã«ãšã£ãŠæ¬¡ã®çºèŠã¯FlexGetã§ããã éåžžã«åŒ·åãªããŒã«ã lostfilm.tvã®ãµããŒãã¯ãããŸããã§ãããããããå°ç¡ãã«ããã®ã¯ãŸã åéºã§ããã ããã§ãªããã°ãããŸããããŸããããrutrackerã§ãã¬ã³ãã®å€åãç£èŠããæ¹æ³ãæããããšã«æåããŸããã§ããã ããããããã¯ä»ã¯ã§ããŸããã ããããç§ã¯ãã®æ ç»ãšæšå¹Žã®æ ç»ãã«ãŒã¿ãŒã§ããŠã³ããŒããã720pã®å質ïŒã€ã³ã¿ãŒãããã¯ããèš±å¯ããŸããã§ããïŒãš6.0以äžã®imdbè©äŸ¡ãããŠã³ããŒãããæ¥æ¬ããã®æ ç»ãé€å€ããŸããïŒãŸããç§ã¯æ¥æ¬æ ç»ã奜ãã§ã¯ãªããå®å®ããŠé«ãïŒã ããã¯ãã¹ãŠãyamlã®æ°è¡ã§èšè¿°ãããŸããã
é·ãéãäž¡æ¹ã®ãµãŒãã¹ïŒTorrentMonitorãšFlexGetïŒã¯ã«ãŒã¿ãŒäžã§äžŠãã§æ©èœããŠããŸããã
cubietruckã玹ä»ããã2.5 TB 2 TBã®ããŒããã©ã€ããåãä»ããåŸãé»åãã»ãšãã©æ¶è²»ããããã¬ã³ãã宿çã«ããŠã³ããŒããããå°ãããŠãéåžžã«å®çšçãªNASã«ãªããŸããã ã¢ãã€ã«ããããªãŒã¯ãåé»ã®åé¡ãé²ããŸãã çŽ30 Mb / sã®ãã¡ã€ã«ã¢ã¯ã»ã¹é床ã¯å®å®ããŠãããããã¯ç§ã®ã¿ã¹ã¯ã«ååã§ãã TorrentMonitorãšFlexGetã¯cubietruckã«ç§»è¡ããŸããã
ãã ãããµã€ãºããŒãã®ãã¬ã³ããããŠã³ããŒãããéã®åé¡ã¯è§£æ¶ãããŠããŸããã
Monitorrent
ãããŠãæ°ããã·ãªãŒãºã®ããŠã³ããŒããèªååããç¬èªã®ãããžã§ã¯ããäœãããã£ãã®ã§ãã TorrentMonitorã¯PHPã§èšè¿°ãããcurlãåŒã³åºããŠæ°ãããã¬ã³ããããŠã³ããŒãããŸãã cronçµç±ã®phpåŒã³åºãã䜿çšããŠãèµ·åæéãèšå®ããŸãã
ããã«äœ¿ãããã®ããã¹ãŠã€ã³ã¹ããŒã«ãããã£ãã®ã§ãããããŸããããŸããã
ãããMonitorrentãçãŸããæ¹æ³ã§ã ã Pythonã§èªåã«åœ¹ç«ã€äœããæžãããã®ã¢ã€ãã¢ãšããŠã å°æ°ã®ã¹ã¯ãªããã»ããã¯ã«ãŠã³ããããŸããã
ããã¯ãPython 2ã§èšè¿°ããã1ããŒãžã®Webã¢ããªã±ãŒã·ã§ã³ã§ããAngular1.4ãšangle-materialãããã³ããšã³ããšããŠäœ¿çšãããŸãã ãŸããããã¯ãšã³ãã¯falconã䜿çšããŠèšè¿°ãããRESTãµãŒãã¹ã§ãã
ãã¹ãŠã®ãœãŒã¹ã¯githubã«ããã Do You the Fuck You Want to Public Licenseã®äžã§é
åžãããŠããŸã ã
次ã®ãã©ãã«ãŒããµããŒããããããã«ãªããŸããã
ããŠã³ããŒããããã¬ã³ãã¯ã次ã®ãã¬ã³ãã¯ã©ã€ã¢ã³ãã«è¿œå ã§ããŸãã
- ãã©ã³ã¹ããã·ã§ã³
- 倧措氎
- uTorrent
- qbittorrent
ããã§ç§ã®ããŒãºã¯200ïŒ
ã«ãªããŸãïŒäž»ã«3ã€ã®ãã©ãã«ãŒãš2ã€ã®ãã¬ã³ãã¯ã©ã€ã¢ã³ãã®ã¿ã䜿çšããŸãïŒã
ããã³ããšã³ã
äžè¬ã«ãããã¯2ããŒãžã®ã¢ããªã±ãŒã·ã§ã³ã§ãã
ãã°ã€ã³çšã®1ããŒãžã2çªç®-ã¢ããªã±ãŒã·ã§ã³ã®æ®ãã®ããŒãžã å¥ã®ãã°ã€ã³ããŒãžãå¿
èŠãªã®ã¯ãã·ã¹ãã ã«ãã°ã€ã³ããåã«éçãã¡ã€ã«ïŒåçãcssãjsïŒãããŠã³ããŒãã§ããªãããã«ããããã ãã§ãã ç§ã¯ããããåŠæ³çã§ãããŸãæå³ã¯ãããŸããããå°ãå®å
šã ãšæãã®ã奜ãã§ãã
äž¡æ¹ã®ããŒãžã¯åäžã®index.htm
ãã¡ã€ã«ããçæããã gulp-preprocess
ãã©ã°ã€ã³ã䜿çšããŠå€æãããŸãã
ãã¹ãŠã®å€éšjsãã¡ã€ã«ïŒframeworksããã³jsã©ã€ãã©ãªïŒã¯ãããŒã ãããã¯ãŒã¯ã«å±éããããšãã«å€éšããMonitorrentã«ã¢ã¯ã»ã¹ããããããããã«ãCDNããããŒããããŸãã ADSLãèªå®
ã«ãããã¢ããããŒãé床ããã£ã512 kbit / sã®å Žåããã£ã³ãã«ããã§ã«æ¥æµã®ååžã§è©°ãŸã£ãŠãããããå¶éãããé床ã§ããŒã ãããã¯ãŒã¯ãããããã€ã³ã¿ãŒãããããjsãããŠã³ããŒãããæ¹ãã¯ããã«é«éã§ãã ãã¹ãŠã®å
éšjsãã¡ã€ã«ã¯ãã§ã«ããŒã ãããã¯ãŒã¯ããããŠã³ããŒãããå¿
èŠããããããã¯ãã©ãŠã¶ãŒã«ãã£ãŠå®å
šã«ãã£ãã·ã¥ãããŸãã
ãŸããæ®ãã®éä¿¡ã¯RESTãä»ããŠè¡ããããããããã³ããšã³ããšããã¯ãšã³ãã®éã§éä¿¡ãããããŒã¿ã¯ã»ãšãã©ãããŸããã
JWTãä»ããŠè¡ãããæ¿èªã ãããæãæé©ãªèªå¯æè¡ã§ããããã«æããŸãã ã»ãã·ã§ã³ããµãŒããŒã«ä¿åããªãããã«ããã¯ã©ã€ã¢ã³ããä¿åããããŒã¿ã®çš®é¡ã確èªã§ããªãããã«ããŸãã ã¢ããªã±ãŒã·ã§ã³ã§JWTããŸã 䜿çšããŠããªãå Žåã¯ããããè¡ãããšã匷ããå§ãããŸãã ãã€ã¯ããµãŒãã¹ã¢ãŒããã¯ãã£ã§JWTã䜿çšããããšã¯ç¹ã«è¯ãããã§ãã
gulp
ã䜿çšããŠè¡ãããŸãgulp
ã¯grunt
ã«ä»£ãããã®ã§ãã ãã¹ãŠã®jsãã¡ã€ã«ã¯1ã€ã®å€§ããªãã³ãã«ã«æ¥çãããŠããã ãã§ã ãŸã çž®å°ãããŠããŸãã ã ããããã¡ã€ã³ãã¡ã€ã«ã¯app.js
ãšåŒã°ããæåã«æåŸã®jsã«å
¥ãããããã¹ãŠãæ£ããapp.js
ãããŸãã ä»ã®ãã¹ãŠã¯ãè§åºŠããã®DIã®ãããã§æ©èœããŸãã
ä»ãwebpackããã蟌ã¿ãŸãã ããããç§ã¯ããã³ããšã³ãéçºè
ã§ã¯ãªãããã®ãããžã§ã¯ããå§ãŸã£ãã°ããã®é ã¯ããã³ããšã³ãéçºã«ã€ããŠäœãç¥ããŸããã§ããã
åçãªãã©ãŒã çæ
远å ã®å®è£
æ©èœã®äžã§ãåçãã©ãŒã ãçæããããã«å®è£
ããè§åºŠãã£ã¬ã¯ãã£ãã«èšåã§ããŸãã
ãã¹ãŠã®ãã©ã°ã€ã³ã®èšå®ã¯åçŽãªãã©ãŒã ã§ããããšãã°ãTransmissionãšã®æ¥ç¶ãèšå®ããããã®ãã©ãŒã ã¯æ¬¡ã®ããã«ãªããŸãã

ãã®ãã©ãŒã ã¯2è¡ã§æ§æãããåè¡ã«ã¯2ã€ã®ããã¹ããããã¯ããããŸãã host
èŠçŽ ã®é·ãã¯80ïŒ
ã port
ã®é·ãport
20ïŒ
ã§ãã ãŠãŒã¶ãŒåãšãã¹ã¯ãŒãã®ãµã€ãºã50ïŒ
ã®ããã¹ããããã¯ã è§æã«ãã®ãã©ãŒã ãæžãã®ã¯ç°¡åãªäœæ¥ã§ãã
ãã ãããã©ã°ã€ã³ã®éçºãç°¡çŽ åããhtmlã«ç
©ããããã®ã§ã¯ãªããããã¯ãšã³ãããžãã¯ã®äœæã«éäžãããã£ãã®ã§ãã ãã©ã°ã€ã³ã¯ã远å ã®ããŒã¯ã¢ãããã¡ã€ã«ãªãã§ãåäžã®ãã¡ã€ã«ãšããŠé
ä¿¡ããå¿
èŠããããŸãã
ãã©ã°ã€ã³ã³ãŒãã§ãã©ãŒã ã¬ã€ã¢ãŠããèšè¿°ããããã®åçŽãªåœ¢åŒãéçºããŸããã
form = [{ 'type': 'row', 'content': [{ 'type': 'text', 'label': 'Host', 'model': 'host', 'flex': 80 }, { 'type': 'text', 'label': 'Port', 'model': 'port', 'flex': 20 }] }, { 'type': 'row', 'content': [{ 'type': 'text', 'label': 'Username', 'model': 'username', 'flex': 50 }, { 'type': 'password', 'label': 'Password', 'model': 'password', 'flex': 50 }] }]
ããã¯ãäŒéã®èšå®ç·šéãã©ãŒã ã®èª¬æã§ãã ããã§ã¯ã3ã€ã®ããã¹ããããã¯ãšãã¹ã¯ãŒããå
¥åããããã®1ã€ã®ãããã¯ã«ã€ããŠèª¬æããŸãã type
ãšlabel
ããããã£ã®ç®çã¯ãååããæããã§ãã flex
ããããã£ã®ååã®éžæã«å€±æããŸããããããwidth
ãšåŒã¶æ¹ãæ£ç¢ºã§ãã-èŠçŽ ã®é·ããæååå
ã®ããŒã»ã³ããŒãžãšããŠæ±ºå®ããŸãã ã¢ã³ã®ã¥ã©ãŒãããªã¢ã«ãflexboxã䜿çšããŠããŒãžäžã®èŠçŽ ã®ã¬ã€ã¢ãŠããèšè¿°ããããããã®ããã«åœåãããŸããã
ãŠãŒã¶ãŒããã®ãã©ãŒã ã«ããŒã¿ãå
¥åãã[ Save
]ãã¿ã³ãã¯ãªãã¯ããåŸã 次ã®åœ¢åŒã®ã¢ãã«ãããã¯ãšã³ãã«éä¿¡ãããŸãã
{ "host": "myhost", "port": "9091", "username": "username", "password": "******" }
ãã®ã¢ãã«ã®ããããã£åã¯ããã©ãŒã ã®èª¬æã®model
ããããã£ããååŸãããŸãã
ããã«ãããããã¯ãšã³ããã©ã°ã€ã³ã®ããžãã¯ã®ã¿ãèšè¿°ããããšã«éäžããUIã®èšè¿°ãç°¡çŽ åã§ããŸããã ã¢ããªã±ãŒã·ã§ã³ã®ã¢ãã€ã«ããŒãžã§ã³ã§ã¯ããã¹ãŠã®èŠçŽ ãæ¬¡ã
ã«é
眮ãããŸãã 1è¡å
ã®èŠçŽ ã¯è€æ°ã®è¡ã«åå²ãããŸãã ãã®æ©èœã¯ãŸã å®è£
ãããŠããŸããããå°æ¥ç»å ŽããããšãæåŸ
ããŠããŸãã
åœç¶ããã©ãŒã ã®åççæã¯æãæè»ãªãœãªã¥ãŒã·ã§ã³ã§ã¯ãããŸããããæ£ãããšæ£åœåããããšæããŸãã ç§ãã¡ã®ããã³ããšã³ãéçºè
ã¯ä»æ¥ãŸã§ããã«åæããŠããŸããããããã§ããã®æ±ºå®ã«ã€ããŠç§ãšè°è«ããŠããŸãã
Websocket
æåã®ããŒãžã§ã³ã®1ã€ã§ã¯ãWebsocketã§ã®äœæ¥ãå®è£
ãããŸããã æåã«å®å
šã«æã§ã次ã«socket.ioã§ ã
pythonã®Websocketã䜿çšããã«ã¯ãpythonã©ã€ãã©ãªã䜿çšããŠsocket.ioã䜿çšããŸãã ã geventã䜿çšããŠã³ã«ãŒãã³ïŒè»œéã¹ã¬ãããã°ãªãŒã³ã¬ããããã®ä»ã®å€ãã®ååãç§ã¯ããèŠããŠããŸããïŒãäœæããŸãã ããã¯ãWebsocketã䜿çšããã¢ããªã±ãŒã·ã§ã³ã§å¿
èŠãªéåæã¢ããªã±ãŒã·ã§ã³ãäœæããããã®åªããã©ã€ãã©ãªã§ãã
ããããæ®å¿µãªãããpython socket.ioã®å®è£
ã«ã¯ããŒãžã§ã³1.0 ããã倧ããgeventã©ã€ãã©ãªãå¿
èŠã§ãã ãŸãã geventããŒã ã«ãŒã¿ãŒã®å ŽåãããŒãžã§ã³0.13ã®ã¿ããããŸãã ç§èªèº«ãé·ãécubietruckã䜿çšããŠãããšããäºå®ã«ãããããããã«ãŒã¿ãŒã§Monitorrentãå®è¡ããå¯èœæ§ãæé€ããããããŸããã§ããã ãã®ãããRESTã€ã³ã¿ãŒãã§ãŒã¹ã§WebsocketãæŸæ£ããé·ãããŒãªã³ã°ãªã¯ãšã¹ãã«çœ®ãæããå¿
èŠããããŸããã çŸåšããããã¯1ã€ã®å Žæã§ã®ã¿äœ¿çšãããæ°ããã·ãªãŒãºã®çŸåšã®ãã§ãã¯ã®ã¹ããŒã¿ã¹ãååŸããŸãã
ããã¯ãšã³ã
falconã䜿çšããŠPython 2ã§èšè¿°ãããŠããŸãã ãã¡ã«ã³ã³ã¯éåžžã«é«ãããã©ãŒãã³ã¹ãçŽæããç§ã«ãšã£ãŠéåžžã«äŸ¿å©ã ãšæãããŸããã æåã¯Monitorrentã¯cherrypyã§æžããããããããã©ã¹ã³ã§æžãçŽããã ããã«ã䜿çšãã詊ã¿ããããŸããããããŸããããã ãã€ããµã«èœã¡çããŸããã
æ®å¿µãªããã falconã¯ããããRESTãµãŒãã¹ãèšè¿°ããããã®ãã¬ãŒã ã¯ãŒã¯ã§ãããéçãªãã®ãæäŸããå¿
èŠããããŸãã Falconã¯ãåããã©ã¹ã³ãcherrypyãšã¯ç°ãªãããã®ãããªæ©èœãããã«äœ¿çšã§ããããã«ããŸããã ãã®æ©èœãèªåã§å®è£
ããå¿
èŠããããŸããã ããã«ããã®ããã®ãã€ããµã«ã¯ãã¹ãŠã®ããŒã«ããããŸãã
@no_auth class StaticFiles(object): def __init__(self, folder=None, filename=None, redirect_to_login=True): self.folder = folder self.filename = filename self.redirect_to_login = redirect_to_login def on_get(self, req, resp, filename=None): if self.redirect_to_login and not AuthMiddleware.validate_auth(req): resp.status = falcon.HTTP_FOUND resp.location = '/login' return file_path = filename or self.filename if self.folder: file_path = os.path.join(self.folder, file_path) if not os.path.isfile(file_path): raise falcon.HTTPNotFound(description='Requested page not found') mime_type, encoding = mimetypes.guess_type(file_path) etag, last_modified = self._get_static_info(file_path) resp.content_type = mime_type or 'text/plain' headers = {'Date': formatdate(time.time(), usegmt=True), 'ETag': etag, 'Last-Modified': last_modified, 'Cache-Control': 'max-age=86400'} resp.set_headers(headers) if_modified_since = req.get_header('if-modified-since', None) if if_modified_since and (parsedate(if_modified_since) >= parsedate(last_modified)): resp.status = falcon.HTTP_NOT_MODIFIED return if_none_match = req.get_header('if-none-match', None) if if_none_match and (if_none_match == '*' or etag in if_none_match): resp.status = falcon.HTTP_NOT_MODIFIED return resp.stream_len = os.path.getsize(file_path) resp.stream = open(file_path, mode='rb') @staticmethod def _get_static_info(file_path): mtime = os.stat(file_path).st_mtime return str(mtime), formatdate(mtime, usegmt=True)
ããã§ã¯ãMIMEã¿ã€ãã®èªèãè¡ãå¿
èŠããããŸããããŸãããã©ãŠã¶ã®if-modified-sinceããã³if-not-matchããããŒããã§ãã¯ããŠãéçããŒã¿ãæ£ãããã£ãã·ã¥ããå¿
èŠããããŸããã ç§ã¯ãã®ãœãªã¥ãŒã·ã§ã³ããã§ãªãŒããŒãŸãã¯ãã©ã¹ã³ããçã¿ã ãã€ããµã®ããã«æžãçŽããã ãã ãšæããŸãã ç§ã¯åœŒããã€ããµã«ãããšã¯æããªãã®ã§ã圌ãã«ãã«ãªã¯ãšã¹ããéä¿¡ããŸããã§ããã
解決çã¯ç§ã«ã¯ã²ã©ãããã§ãããç§ãã¡ã¯ãŸã ãã£ãšçŸãããã®ãèŠã€ããŠããŸããã
çµã¿èŸŒã¿ã®WSGI falcon WebãµãŒããŒã¯éçºã«ã®ã¿äœ¿çšã§ããããã cherrypyã®WSGIå®è£
ãäžå¿ã«å±éããŠããŸããcherrypyã¯éåžžã«å®å®ããŠããŸãã
d = wsgiserver.WSGIPathInfoDispatcher({'/': app}) server_start_params = (config.ip, config.port) server = wsgiserver.CherryPyWSGIServer(server_start_params, d)
誰ããPythonçšã®åªããé«éã®WSGIãµãŒããŒãç¥ã£ãŠããå Žåã¯ãã³ã¡ã³ãã§å
±æããŠãã ããã Monitorrentã¯Windowsã§ãåäœãããããã¯ãã¹ãã©ãããã©ãŒã ãœãªã¥ãŒã·ã§ã³ãå¿
èŠã§ãã
ããã¯pythonã®æåã®äž»èŠãããžã§ã¯ãã§ãããããå€ãã®æ©èœã¯ããããŸããã ãããããéçãªååŸã¯äžéšã®WSGIãµãŒããŒã«è»¢éããããšãã§ããRESTèŠæ±ã®åŠçã«é¢ãããã¹ãŠã®äœæ¥ã¯falconã«æ®ãå¿
èŠããããŸãã 誰ããããªãã«ãããæ£ããè¡ãæ¹æ³ãæããŠãããã°æè¬ããŸãã
äŸåæ§æ³šå
¥
DIã³ã³ãããªãã§ã©ã®ããã«ç掻ã§ããããçè§£ããã®ã¯é£ããã§ãããPythonã®äžçã§ã¯ãããã䜿çšããããšã¯ç¿æ
£ã§ã¯ãããŸããã ãã®ãããã¯ã«ã¯ãã§ã«å€ãã®ããªããŒããããŸããã æ®å¿µãªãããè¯ã解決çãèŠã€ãããªãã£ãããããã¹ãŠã®ã¯ã©ã¹ãžã®æç€ºçãªäŸåæ§æ³šå
¥ãå©çšããŸããã
ãã©ã°ã€ã³ã·ã¹ãã
ãã¹ãŠã®ãã©ãã«ãŒãšãã¬ã³ãã¯ã©ã€ã¢ã³ãã¯ãã©ã°ã€ã³ãšããŠå®è£
ãããŸãã ãããŸã§ã®ãšããããããã¯ãã¹ãŠã¿ã€ãã®ãã©ã°ã€ã³ã§ãããè¿ãå°æ¥ãéç¥çšã®ãã©ã°ã€ã³ãæäŸãããäºå®ã§ãã 察å¿ãããã«ãªã¯ãšã¹ãã¯ã¬ãã¥ãŒåŸ
ã¡ã§ãããããŒãžã§ã³1.1ã§å©çšå¯èœã«ãªããŸãã
ãã©ã«ããŒãã¹ãã£ã³ãããããããã¹ãŠã®ã¯ã©ã¹ãèªã¿èŸŒãããšãã§ãããã©ã°ã€ã³ãèªã¿èŸŒãããã®çŸããã·ã¹ãã ãèŠã€ãããªãã£ããããFlexGetããå®è£
ã®ã¢ã€ãã¢ãçãŸããŸããã
åãã©ã°ã€ã³ã¯ãã·ã¹ãã ã«èªåèªèº«ãç»é²ããŸãã ç§ã«ã¯æããŸãããã·ã¹ãã ããã©ã°ã€ã³ãæ€çŽ¢ããæ¹ãæ£ç¢ºã§ããããã©ã°ã€ã³ãã·ã¹ãã ã«èªåèªèº«ãç»é²ããæ¹æ³ãç¥ã£ãŠããããã§ã¯ãããŸããã
ãã¬ã³ãã¯ã©ã€ã¢ã³ãã®ãã©ã°ã€ã³
ãã©ã°ã€ã³ã€ã³ã¿ãŒãã§ã€ã¹ã¯éåžžã«ã·ã³ãã«ã§ãã
class MyClientPlugin(object): name = "myclient" form = [{ ... }] def get_settings(self): pass def set_settings(self, settings): pass def check_connection(self): pass def find_torrent(self, torrent_hash): pass def add_torrent(self, torrent): pass def remove_torrent(self, torrent_hash): pass register_plugin('client', 'myclient', MyClientPlugin())
ã¡ãœããã¯2ã€ã®ã°ã«ãŒãã«åããããŸãã1ã€ã¯ã¯ã©ã€ã¢ã³ãã®ãã¬ã³ãèšå®ãä¿åããã°ã«ãŒãããã1ã€ã¯ãã¬ã³ãã管çããã°ã«ãŒãã§ãã
set_settings()
ããã³get_settings()
ã¡ãœããã¯ãããŒã¿ããŒã¹ããããŒã¿ãä¿åããã³èªã¿åããŸãã
*_torrent()
ã¡ãœããã¯ããŠã³ããŒããå¶åŸ¡ããŸãã ãã¬ã³ããã¡ã€ã«ã¯ããã·ã¥ã³ãŒãã«ãã£ãŠäžæã«èå¥ã§ãããããæ¢ã«ããŠã³ããŒããããããã€ã®æšéЬãåé€ããŠæ€çŽ¢ããã«ã¯ããã¬ã³ãã®ããã·ã¥ã転éããã ãã§ãã ãããããã¬ã³ãã远å ããã«ã¯ããã¹ãŠã®ãã¬ã³ãã転éããå¿
èŠãããã®ã¯åœç¶ã§ãã
ãã¬ã³ããã¡ã€ã«ãè§£æããããã®ã©ã€ãã©ãªã¯ãFlexGetããååŸãããŸããã 圌女ãã©ãããæ¥ãã®ãããããŸããã§ããïŒäžçæžåœåªåããŸããã§ãããïŒã Python 3ããµããŒãããã¯ãªãŒã³ã§ã¢ã»ã³ãã«ãããŠããªããã€ãé
åãèªã¿åãããã«ãããã€ãã®å°ããªå€æŽãå ããããŸããã
form
ãã£ãŒã«ãã¯ãUIã§ã®ãã®ãã©ã°ã€ã³ã®èšå®ãã©ãŒã ã説æããŸãã ãããã©ã®ããã«æ©èœãããã«ã€ããŠã¯ãåçãã©ãŒã çæã®ã»ã¯ã·ã§ã³ãã芧ãã ããã
ãã©ã°ã€ã³ã¯ååã«ã³ã³ãã¯ãã§å®è£
ãç°¡åã§ãã ããšãã°ãæ°è¡ã®ã³ã¡ã³ããš7è¡ã®ã€ã³ããŒããå«ããŠãéä¿¡ã«ãããã®ã¯115è¡ã®ã¿ã§ãã
ãã©ãã«ãŒãã©ã°ã€ã³
Monitorrentã®èгç¹ã§ã¯ããã¬ã³ããžã®å€æŽã®ãµãã¹ã¯ãªãã·ã§ã³ã¯ãããã¯ãšåŒã°ããŸãã ããšãã°ãlostfilmã§ã¯ãRSSãè§£æããã®ã§ã¯ãªãã圌ã®ããŒãžã®ã·ãªãŒãºã®å€æŽãç£èŠããŸãã æ°ããã·ãªãŒãºã®ãªãªãŒã¹åŸãä¿®æ£ããããã¡ã€ã«ã§ã¯ãªããæ°ãããã¬ã³ããã¡ã€ã«ãããŠã³ããŒãããŸãã ãããã£ãŠããµãã¹ã¯ãªãã·ã§ã³ãããã¯ãåŒã³åºãæ¹ãåççã ãšæããŸãã
ãã¬ã³ãã¯ã©ã€ã¢ã³ãã®ãã©ã°ã€ã³ã³ã³ãã©ã¯ããéåžžã«åçŽã§ããããã®ããã®åºæ¬ã¯ã©ã¹ããªãå Žåããã©ãã«ãŒã«ãšã£ãŠã¯ãã¹ãŠãããè€éã«ãªããŸãã ãŸãããã©ãã«ãŒçšã®ã·ã³ãã«ãªãã©ã°ã€ã³ã€ã³ã¿ãŒãã§ã€ã¹ãæ€èšããŸãã
class TrackerPluginBase(with_metaclass(abc.ABCMeta, object)): topic_form = [{ ... }] @abc.abstractmethod def can_parse_url(self, url): pass def prepare_add_topic(self, url): pass def add_topic(self, url, params): pass def get_topics(self, ids): pass def save_topic(self, topic, last_update, status=Status.Ok): pass def get_topic(self, id): pass def update_topic(self, id, params): pass @abc.abstractmethod def execute(self, topics, engine): pass
ç¹å®ã®ãã¬ã³ã*_topic()
ã®èšå®ãæäœããã¡ãœãããšããã¹ãŠã®get_topics()
ããŒããååŸããå¥ã®ã¡ãœããããããŸãã
ç£èŠçšã®æ°ãããã¬ã³ãã®è¿œå ã¯ããããã¯URLã§è¡ãããŸãã ããšãã°ãrutrackerã®å Žåãããã¯ãã©ãŒã©ã ããŒãžã®ã¢ãã¬ã¹ã§ãããlostfilmã®å Žåãããã¯ã·ãªãŒãºããŒãžã§ãã ãã®ãã©ã°ã€ã³ããã®URLãåŠçã§ãããã©ããã調ã¹ãããã«ããã¹ãŠã®ãã©ã°ã€ã³ã«å¯ŸããŠcan_parse_url()
ã¡ãœãããåŒã³åºããããã®URLã§åäœãããã©ãããæ£èŠè¡šçŸã§ç¢ºèªããŸãã ãã®ãããªãã©ã°ã€ã³ãèŠã€ãããªãå ŽåããŠãŒã¶ãŒã«ã¯ãããã¯ã远å ã§ããªãã£ããšããã¡ãã»ãŒãžã衚瀺ãããŸãã ãã®URLãçè§£ãããã©ã°ã€ã³ãèŠã€ãã£ãå ŽåããŸãprepare_add_topic()
ã¡ãœãããåŒã³åºããŸãããã®ã¡ãœããã¯ãè§£æãããããŒã¿ãå«ãã¢ãã«ãè¿ãããŠãŒã¶ãŒããã®ããŒã¿ãç·šéã§ããããã«ããŸãã ããŒã¿ç·šéãã©ãŒã ã¯ã topic_form
ãã£ãŒã«ãã§èª¬æãããŠtopic_form
ãŸãã ãŠãŒã¶ãŒadd_topic
ãããã¯ããŒã¿ãadd_topic
[ 远å ]ãã¿ã³ãã¯ãªãã¯ãããšã add_topic
ã¡ãœãããadd_topic
ãŸãããã®ã¡ãœããã«ç·šéæžã¿ã¢ãã«ã転éããããã®ãããã¯ãç£èŠããŒã¹ã«ä¿åãããŸãã
ããã§ããã¹ãŠã®ãããã¯ã«å
±éã®ããããã£display_name
ã1ã€ãããŸãã ã¡ã€ã³ããŒãžã«è¡šç€ºãããã¿ã€ãã«ã lostfilmã®å ŽåãããŠã³ããŒãããã·ãªãŒãºã®å質ãéžæã§ããŸãã
æå€§ãã€æãéèŠãªæ¹æ³ã¯ã execute(self, topics, engine)
ã§ãã 圌ã¯ã倿Žã®ç¢ºèªãšæ°ãããšããœãŒãã®ããŠã³ããŒããæ
åœããŠããŸãã æ€èšŒçšã®ãããã¯ã®ãªã¹ããšç¹å¥ãªengine
ãªããžã§ã¯ããåœŒã«æž¡ãããŸãã engine
ãªããžã§ã¯ãã䜿çšãããšããã¬ã³ãã¯ã©ã€ã¢ã³ãã«æ°ãããã¬ã³ãã远å ã§ãããã®ã³ã°çšã®ãªããžã§ã¯ããæäŸããŸãã ããŠã³ããŒãã§ãããã¬ã³ãã¯ã©ã€ã¢ã³ãã¯1ã€ã ãã§ãã ãã©ã°ã€ã³ã¯ãã®ã¯ã©ã€ã¢ã³ããã¯ã©ã€ã¢ã³ãã§ãããã©ãããæ°ã«ããŸããã engine
ã¯ã¯ã©ã€ã¢ã³ããéžæãã責任ãããããã©ã°ã€ã³ã¯ããŠã³ããŒããããã¬ã³ããengine
転éããã ãã§ãã æ°ããã·ãªãŒãºã远å ããŠã·ãªãŒãºãé
åžãããå Žåã engine
ã¯ä»¥åã®é
åžãåé€ããæ°ããé
åžã远å ããŸãã
äžéšã®ãã©ãã«ãŒã«ã¯èªèšŒãå¿
èŠãªããã WithCredentialsMixin
ãã°ã€ã³ã®æ
å ±ãä¿åã§ããå¥ã®ã¿ã€ãã®ãã©ã°ã€ã³ãWithCredentialsMixin
ãŸãã ååã瀺ãããã«ããã®ã¯ã©ã¹ã¯ããã¯ã¹ã€ã³ã§ãïŒæ£ç¢ºãªããã¯ã¹ã€ã³ã«ã€ããŠã¯ä»¥äžã§èª¬æããŸãïŒã çŸåšããããã®ã¿ã€ãã®ãã©ã°ã€ã³ã®ã¿ã«UIãã»ããã¢ãããããŠããŸãã ãã®ã¯ã©ã¹ã¯ããã©ã°ã€ã³ã€ã³ã¿ãŒãã§ã€ã¹ã«ããã«ããã€ãã®ã¡ãœããã远å ããŸãã
class WithCredentialsMixin(with_metaclass(abc.ABCMeta, TrackerPluginMixinBase)): credentials_form = [{ ... }] @abc.abstractmethod def login(self): pass @abc.abstractmethod def verify(self): pass def get_credentials(self): pass def update_credentials(self, credentials): pass def execute(self, ids, engine): if not self._execute_login(engine): return super(WithCredentialsMixin, self).execute(ids, engine) def _execute_login(self, engine): pass
èªèšŒããŒã¿*_credentials
ãä¿åããã³ããŒãããããã®ã¡ãœããã å
¥åãããããŒã¿ã®ãã°ã€ã³ããã³æ€èšŒã®ã¡ãœããlogin()
ããã³verify()
ã ãŸãã execute()
ã¡ãœããããªãŒããŒã©ã€ãããŠãæåã«ãã©ãã«ãŒã«ãã°ã€ã³ãïŒ _execute_login()
ã¡ãœãããåŒã³åºããŠïŒããã®åŸããããã¯ã®å€æŽã確èªããŸãã
èšå®ãç·šéããã«ã¯ã credentials_form
ãã£ãŒã«ãããåçã«çæããããã©ãŒã ã䜿çšãããŸãã
çŸåšãlostfilmãé€ããã¹ãŠã®ãã©ãã«ãŒã®å€æŽãã§ãã¯ã¯ããã¬ã³ããã¡ã€ã«ãããŠã³ããŒããããã®ããã·ã¥ãååããŠã³ããŒããããã®ãšæ¯èŒããããšã«ãã£ãŠå®è¡ãããŸãã ããã·ã¥ãç°ãªãå Žåãæ°ãããã¬ã³ããããŠã³ããŒããããã¯ã©ã€ã¢ã³ãããã¬ã³ãã«è¿œå ãããŸãã ãããããHEADãªã¯ãšã¹ããªã©ãéä¿¡ããŠããŒãžèªäœããã§ãã¯ããã ãã§ååã§ãããããã®ãªãã·ã§ã³ã®æ¹ãä¿¡é Œæ§ãé«ããªããŸãã 倿ããããã«ãããŒãžãµã€ãºã¯ãã¬ã³ããã¡ã€ã«èªäœããã倧ããããã¬ã³ãã®å€æŽã§ã¯ãªãã³ã¡ã³ãã远å ããã ãã§ããŒãžã倿ŽãããŸãã ããã«ãrutorã¯HEADããŸã£ãããµããŒãããŠããŸããã§ããã
ãã®ããžãã¯ã¯ã ExecuteWithHashChangeMixin
ã¯ã©ã¹ã®execute
ã¡ãœããã«é
眮ãããŸãã ãããWithCredentialsMixin
ãããªmixin WithCredentialsMixin
ã ããã«ããããã©ãã«ãŒã«å¿ããŠ1ã€ãŸãã¯2ã€ã®ããã¯ã¹ã€ã³ãç¶æ¿ããããã€ãã®ã¡ãœããã®ã¿ãåå®çŸ©ãããã©ã°ã€ã³ãäœæã§ããŸãã
ãããfree-torrents.orgã®ãã©ã°ã€ã³ã®å®çŸ©æ¹æ³ã§ãïŒ
class FreeTorrentsOrgPlugin(WithCredentialsMixin, ExecuteWithHashChangeMixin, TrackerPluginBase): ... topic_form = [{ ... }] def login(self): pass def verify(self): pass def can_parse_url(self, url): return self.tracker.can_parse_url(url) def parse_url(self, url): return self.tracker.parse_url(url) def _prepare_request(self, topic): headers = {'referer': topic.url, 'host': "dl.free-torrents.org"} cookies = self.tracker.get_cookies() request = requests.Request('GET', self.tracker.get_download_url(topic.url), headers=headers, cookies=cookies) return request.prepare()
çµæãšããŠãåå®çŸ©ãå¿
èŠãªã¡ãœããã¯2ã3ã ãã§ãããæ°ãããã¬ã³ãããã§ãã¯ããããã®æãè€éãªããžãã¯ã¯å€æŽãããã«ã WithCredentialsMixin
ããã³ExecuteWithHashChangeMixin
éäžããŠããŸãã
rutor.orgã®ãã©ã°ã€ã³ã¯ExecuteWithHashChangeMixinã®ã¿ã䜿çšãExecuteWithHashChangeMixin
ã
class RutorOrgPlugin(ExecuteWithHashChangeMixin, TrackerPluginBase): pass
ãŸããlostfilmã®ãã©ã°ã€ã³ã¯ã倿ŽãèŠã€ããããã®ç¬èªã®å®è£
ããããããWithCredentialsMixinã®ã¿ã䜿çšããŸãã
class LostFilmPlugin(WithCredentialsMixin, TrackerPluginBase): pass
lostfilmã®ãã©ã°ã€ã³ã¯éåžžã«è€éã§ã640è¡ããããŸãã bogiãä»ãããã°ã€ã³ã¯ç¹ã«è€éã§ããããã¹ãŠã7ãæä»¥äžã«ããã£ãŠæèšã®ããã«æ©èœããŠããŸãã
ä»ã®èšèªã§ã¯ãããã¯ãããã«ç°ãªãæ¹æ³ã§å®è£
ãããŸãããPythonã§ã¯è€æ°ã®ç¶æ¿ã䜿çšã§ããããšãå¬ããæããŸããããã¯ãããã¯ã¹ã€ã³ãä»ããŠã®ã¿è¡ãå¿
èŠããããŸãã ãŸããç¶æ¿ããããã¹ãŠã®ã¯ã©ã¹ã®æ£ããé åºãæç¢ºã«ç€ºãå¿
èŠããããŸãã ããã¯ãããããå€éç¶æ¿ãã³ãŒãã®èšè¿°ã容æã«ããããã«æããå¯äžã®ã±ãŒã¹ã§ãã
æ®å¿µãªãããrutorã¯ãã£ã¹ããªãã¥ãŒã·ã§ã³ãåé€ããå Žåããããæ°ãããã£ã¹ããªãã¥ãŒã·ã§ã³ãæ¢ãå¿
èŠããããŸããMonitorrentã¯ãªã¢ãŒããã£ã¹ããªãã¥ãŒã·ã§ã³ã远跡ãããã®ãããªãããã¯ãã¡ã€ã³ç»é¢ã§ãŠãŒã¶ãŒã«åŒ·èª¿è¡šç€ºã§ããŸãã execute()
ã¡ãœããããã®ããžãã¯ãæ
åœããŸãã
ããŒã¿ããŒã¹
ããŒã¿ããŒã¹ã¯sqliteã§ãã ä»ã®ããŒã¿ããŒã¹ããµããŒãããããã®ãã±ããããããŸããããããå¿
èŠã ãšã¯æããŸãããããã¯ã·ã¹ãã ã®è€éããããã»ã©å¢å ãããŸããããããŸããŸãªããŒã¿ããŒã¹ããã¹ãããããã«å€ãã®ãã¹ããæžãå¿
èŠããããŸãã ããã«ãsqliteã«å³å¯ã«çµã³ä»ããããå°éã®ã³ãŒãããããŸãã
sqlalchemy㯠ORMãšããŠäœ¿çšãããŸãã ããã¯åŒ·åã§äŸ¿å©ãªORMã§ãããã¯ã©ã¹ãžã®ãããã³ã°ã«ããã¯ã©ã¹ã®ç¶æ¿ããµããŒãããŸãã ãã®sqlalchemyã¯ããã€ãä»ã®ããŒã¿ããŒã¹ã®ãµããŒãã远å ããå Žåãå¥ã®ããŒã¿ããŒã¹ãžã®ç§»è¡ãç°¡çŽ åããŸãã
Monitorrentã®ã³ãŒãã¯ãããŒã¿ãšã¹ããŒãã®ç§»è¡ããµããŒãããŠããŸãã æ®å¿µãªããã ããã«äœ¿ãã sqlalchemyã«ã¯ãã®æ©èœã¯ãããŸãããã sqlachemyã®äœè
ã«ããå¥ã®ãããžã§ã¯ã-alembicãããããã®ç®çã®ããã«äœ¿çšããŠããŸãã
Pythonçšã®sqliteãã©ã€ããŒã«ã¯ãããã€ãã®å¶éããããŸãã ãã®1ã€ã¯ãããŒã¿ã¹ããŒãã®å€æŽãšäžç·ã«ãã©ã³ã¶ã¯ã·ã§ã³ã䜿çšã§ããªãããšã§ãã ããã¯ãããŒã¿ããŒã¹ãæ°ããããŒãžã§ã³ã«ç§»è¡ãããšãã«éèŠãªå ŽåããããŸãã ãã®åé¡ã®è§£æ±ºçã¯ã sqlalchemy Webãµã€ãã§èª¬æãããŠããŸã ã ããããããã®ã³ãŒãã¯Monitorrentã«ç§»æ€ãããŸãã ã çŸåšãç§»è¡ã¯åé¡ãªãæ©èœããŸãã
ãã©ãã«ãŒã®ã»ãšãã©ãã¹ãŠã®ãã©ã°ã€ã³ã¯ãæãå€ãããŒãžã§ã³ããææ°ãªãªãŒã¹ãžã®ç§»è¡ããã§ã«ååŸããŠããŸãã ç§»è¡ãµããŒãã¯ããã©ã°ã€ã³ãç»é²ããããšãã«æž¡ãããupgrade
æ¹æ³ãéããŠå®è£
ãããŸãã
upgrade
æ¹æ³ã§ã¯ãæåã«åãããŒãã«ã®ååšãªã©ã®ããŸããŸãªãã§ãã¯ã«ãã£ãŠçŸåšã®ããŒãžã§ã³ã倿ãããã®ããŒãžã§ã³ããææ°ããŒãžã§ã³ã«çŽæ¥ç§»è¡ããŸãã
rutorã®ãµã³ãã«ç§»è¡ã³ãŒãïŒ
def upgrade(engine, operations_factory): if not engine.dialect.has_table(engine.connect(), RutorOrgTopic.__tablename__): return version = get_current_version(engine) if version == 0: upgrade_0_to_1(engine, operations_factory) version = 1 if version == 1: upgrade_1_to_2(operations_factory) version = 2 def get_current_version(engine): m = MetaData(engine) t = Table(RutorOrgTopic.__tablename__, m, autoload=True) if 'url' in t.columns: return 0 if 'hash' in t.columns and not t.columns['hash'].nullable: return 1 return 2 def upgrade_0_to_1(engine, operations_factory): m0 = MetaData() rutor_topic_0 = Table("rutororg_topics", m0, Column('id', Integer, primary_key=True), Column('name', String, unique=True, nullable=False), Column('url', String, nullable=False, unique=True), Column('hash', String, nullable=False), Column('last_update', UTCDateTime, nullable=True)) m1 = MetaData() topic_last = Table('topics', m1, *[c.copy() for c in Topic.__table__.columns]) rutor_topic_1 = Table('rutororg_topics1', m1, Column("id", Integer, ForeignKey('topics.id'), primary_key=True), Column("hash", String, nullable=False)) def topic_mapping(topic_values, raw_topic): topic_values['display_name'] = raw_topic['name'] with operations_factory() as operations: if not engine.dialect.has_table(engine.connect(), topic_last.name): topic_last.create(engine) operations.upgrade_to_base_topic(rutor_topic_0, rutor_topic_1, PLUGIN_NAME, topic_mapping=topic_mapping)
æåã®ããŒãžã§ã³ã®1ã€ã§ã¯ããã¹ãŠã®ãã©ã°ã€ã³ã«ãããã¯ãä¿åããããã®ç¬èªã®ããŒãã«ããããŸããã åŸã§ã url
ãdisplay_name
ãªã©ã®äžè¬çãªãã£ãŒã«ãã¯ãããã¯ããŒãã«ã«ç§»åãããŸããã ã³ãŒãã§ã¯ãããã¯ããŒã¹ãããã¯ã¯ã©ã¹ãããããã¯ã®ãã¹ãŠã®ã¯ã©ã¹ãç¶æ¿ããããã«å®è£
ãããŸãã
, topics. , MonitorrentOperations.upgrade_to_base_topic
:
def upgrade_to_base_topic(self, v0, v1, polymorphic_identity, topic_mapping=None, column_renames=None): from .plugins import Topic self.create_table(v1) topics = self.db.query(v0) for topic in topics: raw_topic = row2dict(topic, v0) # insert into topics topic_values = {c: v for c, v in list(raw_topic.items()) if c in Topic.__table__.c and c != 'id'} topic_values['type'] = polymorphic_identity if topic_mapping: topic_mapping(topic_values, raw_topic) result = self.db.execute(Topic.__table__.insert(), topic_values) # get topic.id inserted_id = result.inserted_primary_key[0] # insert into v1 table concrete_topic = {c: v for c, v in list(raw_topic.items()) if c in v1.c} concrete_topic['id'] = inserted_id if column_renames: column_renames(concrete_topic, raw_topic) self.db.execute(v1.insert(), concrete_topic) # drop original table self.drop_table(v0.name) # rename new created table to old one self.rename_table(v1.name, v0.name)
, 2.5 . , . .
DBSession
python with, :
with DBSession() as db: cred = db.query(self.credentials_class).first() cred.c_uid = self.tracker.c_uid cred.c_pass = self.tracker.c_pass cred.c_usess = self.tracker.c_usess
FlexGet'. DBSession()
. .
Monitorrent . 2 , .
threading.Thread
. threading.Timer
, stop()
, . , . , 2 ( ) .
Monitorrent ' server.py
. cherrypy 6687 .
:
debug
â , JWT , . false. .ip
â , . 0.0.0.0.port
â , . 6687db_path
â . monitorrent.db. ã€ãŸã .onfig
â , . config.py
3- .
, .
â (config.py ). python , .
python exec
. python 3 exec_
six .
with open(config_path) as config_file: six.exec_(compile(config_file.read(), config_path, 'exec'), {}, parsed_config)
- , , .
â : MONITORRENT_DEBUG
, MONITORRENT_IP
, MONITORRENT_PORT
MONITORRENT_DB_PATH
. .
, , .
docker , .
config.py, .
python 100%. â server.py
. . â . .
100% , .
unittest python.
, . Sqlite , . , , , , .
, , .
Monitorrent ' â .
vcrpy , , . monkey requests , . ã€ãŸã back-end, , , . , back-end . , . , , .
. html . , , 404 . httpretty . httpretty , , .
, vcrpy FlexGet'. 97% vcrpy httpretty .
2 , . coveralls.io codecov.io .
, lostfilm, html:
parser = None # lxml have some issue with parsing lostfilm on Windows if sys.platform == 'win32': parser = 'html5lib' soup = get\_soup(r.text, parser)
, Linux 100%. coveralls.io, codecov.io . . :
# lxml have some issue with parsing lostfilm on Windows, so replace it on html5lib for Windows soup = get\_soup(r.text, 'html5lib' if sys.platform == 'win32' else None)
python , . , . . . codecov.io Chrome, github, .
front-end . . - , .
, vcrpy , .. back-end'.
ãã«ããµãŒããŒ
Monitorrent â . 2 : Windows â ci.appveyor.com , â travis-ci.org Linux. Appveyor Windows . â travis, , coveralls.io codecov.io.
drone.io docker x86/x64 ARM. , . , .
. git flow github. master , issue. develop.
Semantic Versioning . â 1.0.0. 4 , .
ZenHub Chrome & Firefox, Boards Burndown github issue. waffle.io , ZenHub .
ãããã«
Monitorrent 9 .
. . , . Monitorrent , , requests. - . Windows, cubietruck. .
. 10 , , UI .
(, UX ), , .
, github. , pull request' , github'. .
, . Monitorrent .