トレントモニタリングず自動ダりンロヌド

最近では、トレントから新しいシリヌズをダりンロヌドするプロセスを自動化する方法に関する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の䞋で配垃されおいたす 。


次のトラッカヌがサポヌトされるようになりたした。



ダりンロヌドしたトレントは、次のトレントクラむアントに远加できたす。



これで私のニヌズは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 .


:



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 .



Source: https://habr.com/ru/post/J305574/


All Articles