トレントモニタリングと自動ダウンロード

最近では、トレントから新しいシリーズをダウンロードするプロセスを自動化する方法に関する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で記述されています。 ファルコンは非常に高いパフォーマンスを約束し、私にとって非常に便利だと思われました。 最初はMonitorrentcherrypyで書かれ、それからフラスコで書き直され、 ボトルを使用する試みがありましたが、うまくいかず、 ハヤブサに落ち着きました。







残念ながら、 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 .








All Articles