Integrated pytaw library for youtube API.

This commit is contained in:
Tiberiu Chibici 2018-10-29 18:52:09 +02:00
parent 6dd63b078f
commit 0fb09b00da
35 changed files with 3067 additions and 1705 deletions

View File

@ -2,7 +2,17 @@
<project version="4"> <project version="4">
<component name="dataSourceStorageLocal"> <component name="dataSourceStorageLocal">
<data-source name="Django default" uuid="2dac2136-d902-4d27-8789-9371934602fd"> <data-source name="Django default" uuid="2dac2136-d902-4d27-8789-9371934602fd">
<database-info product="SQLite" version="3.20.1" jdbc-version="2.1" driver-name="SQLite JDBC" driver-version="3.20.1.1" family="SQLITE" exact-version="3.20.1" /> <database-info product="SQLite" version="3.25.1" jdbc-version="2.1" driver-name="SQLite JDBC" driver-version="3.25.1" family="SQLITE" exact-version="3.25.1">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<auth-required>false</auth-required>
<introspection-schemas>*:@</introspection-schemas>
</data-source>
<data-source name="db" uuid="77df9da5-0b97-445e-a895-744ef8257a74">
<database-info product="SQLite" version="3.25.1" jdbc-version="2.1" driver-name="SQLite JDBC" driver-version="3.25.1" family="SQLITE" exact-version="3.25.1">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" /> <case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<auth-required>false</auth-required> <auth-required>false</auth-required>
<introspection-schemas>*:@</introspection-schemas> <introspection-schemas>*:@</introspection-schemas>

17
.idea/dataSources.xml generated
View File

@ -12,5 +12,22 @@
<property name="enable_load_extension" value="true" /> <property name="enable_load_extension" value="true" />
</driver-properties> </driver-properties>
</data-source> </data-source>
<data-source source="LOCAL" name="db" uuid="77df9da5-0b97-445e-a895-744ef8257a74">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite3</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/sqlite-jdbc-3.25.1.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/license.txt</url>
</library>
</libraries>
</data-source>
</component> </component>
</project> </project>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<dataSource name="Django default"> <dataSource name="Django default">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.10"> <database-model serializer="dbm" rdbms="SQLITE" format-version="4.11">
<root id="1"> <root id="1">
<ServerVersion>3.20.1</ServerVersion> <ServerVersion>3.25.1</ServerVersion>
</root> </root>
<schema id="2" parent="1" name="main"> <schema id="2" parent="1" name="main">
<Current>1</Current> <Current>1</Current>
@ -11,868 +11,28 @@
<collation id="3" parent="1" name="BINARY"/> <collation id="3" parent="1" name="BINARY"/>
<collation id="4" parent="1" name="NOCASE"/> <collation id="4" parent="1" name="NOCASE"/>
<collation id="5" parent="1" name="RTRIM"/> <collation id="5" parent="1" name="RTRIM"/>
<table id="6" parent="2" name="YtManagerApp_channel"/> <table id="6" parent="2" name="sqlite_master">
<table id="7" parent="2" name="YtManagerApp_subscription"/>
<table id="8" parent="2" name="YtManagerApp_subscriptionfolder"/>
<table id="9" parent="2" name="YtManagerApp_usersettings"/>
<table id="10" parent="2" name="YtManagerApp_video"/>
<table id="11" parent="2" name="auth_group"/>
<table id="12" parent="2" name="auth_group_permissions"/>
<table id="13" parent="2" name="auth_permission"/>
<table id="14" parent="2" name="auth_user"/>
<table id="15" parent="2" name="auth_user_groups"/>
<table id="16" parent="2" name="auth_user_user_permissions"/>
<table id="17" parent="2" name="django_admin_log"/>
<table id="18" parent="2" name="django_content_type"/>
<table id="19" parent="2" name="django_migrations"/>
<table id="20" parent="2" name="django_session"/>
<table id="21" parent="2" name="sqlite_master">
<System>1</System> <System>1</System>
</table> </table>
<table id="22" parent="2" name="sqlite_sequence"> <column id="7" parent="6" name="type">
<System>1</System>
</table>
<column id="23" parent="6" name="id">
<Position>1</Position> <Position>1</Position>
<DataType>integer|0s</DataType> <DataType>text|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column> </column>
<column id="24" parent="6" name="channel_id"> <column id="8" parent="6" name="name">
<Position>2</Position> <Position>2</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column> </column>
<column id="25" parent="6" name="username"> <column id="9" parent="6" name="tbl_name">
<Position>3</Position> <Position>3</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
</column> </column>
<column id="26" parent="6" name="custom_url"> <column id="10" parent="6" name="rootpage">
<Position>4</Position> <Position>4</Position>
<DataType>text|0s</DataType> <DataType>int|0s</DataType>
</column> </column>
<column id="27" parent="6" name="name"> <column id="11" parent="6" name="sql">
<Position>5</Position> <Position>5</Position>
<DataType>text|0s</DataType> <DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="28" parent="6" name="description">
<Position>6</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="6" name="icon_default">
<Position>7</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="6" name="icon_best">
<Position>8</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="31" parent="6" name="upload_playlist_id">
<Position>9</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="32" parent="6" name="sqlite_autoindex_YtManagerApp_channel_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>channel_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="33" parent="6" name="sqlite_autoindex_YtManagerApp_channel_2">
<NameSurrogate>1</NameSurrogate>
<ColNames>username</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="34" parent="6" name="sqlite_autoindex_YtManagerApp_channel_3">
<NameSurrogate>1</NameSurrogate>
<ColNames>custom_url</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="35" parent="6">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="36" parent="6">
<ColNames>channel_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_1</UnderlyingIndexName>
</key>
<key id="37" parent="6">
<ColNames>username</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_2</UnderlyingIndexName>
</key>
<key id="38" parent="6">
<ColNames>custom_url</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_3</UnderlyingIndexName>
</key>
<column id="39" parent="7" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="40" parent="7" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="41" parent="7" name="playlist_id">
<Position>3</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="42" parent="7" name="description">
<Position>4</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="43" parent="7" name="icon_default">
<Position>5</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="7" name="icon_best">
<Position>6</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="7" name="auto_download">
<Position>7</Position>
<DataType>bool|0s</DataType>
</column>
<column id="46" parent="7" name="download_limit">
<Position>8</Position>
<DataType>integer|0s</DataType>
</column>
<column id="47" parent="7" name="download_order">
<Position>9</Position>
<DataType>text|0s</DataType>
</column>
<column id="48" parent="7" name="manager_delete_after_watched">
<Position>10</Position>
<DataType>bool|0s</DataType>
</column>
<column id="49" parent="7" name="channel_id">
<Position>11</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="50" parent="7" name="parent_folder_id">
<Position>12</Position>
<DataType>integer|0s</DataType>
</column>
<column id="51" parent="7" name="user_id">
<Position>13</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="52" parent="7" name="sqlite_autoindex_YtManagerApp_subscription_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>playlist_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="53" parent="7" name="YtManagerApp_subscription_channel_id_b83c6f21">
<ColNames>channel_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="54" parent="7" name="YtManagerApp_subscription_parent_folder_id_c4c64c21">
<ColNames>parent_folder_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="55" parent="7" name="YtManagerApp_subscription_user_id_9d38617d">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="56" parent="7">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="57" parent="7">
<ColNames>playlist_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_subscription_1</UnderlyingIndexName>
</key>
<foreign-key id="58" parent="7">
<ColNames>channel_id</ColNames>
<RefTableName>YtManagerApp_channel</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="59" parent="7">
<ColNames>parent_folder_id</ColNames>
<RefTableName>YtManagerApp_subscriptionfolder</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="60" parent="7">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="61" parent="8" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="62" parent="8" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="63" parent="8" name="user_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="64" parent="8" name="parent_id">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<index id="65" parent="8" name="YtManagerApp_subscriptionfolder_user_id_6fb12da0">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="66" parent="8" name="YtManagerApp_subscriptionfolder_parent_id_bd5f4bc1">
<ColNames>parent_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="67" parent="8">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="68" parent="8">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="69" parent="8">
<ColNames>parent_id</ColNames>
<RefTableName>YtManagerApp_subscriptionfolder</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="70" parent="9" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="71" parent="9" name="mark_deleted_as_watched">
<Position>2</Position>
<DataType>bool|0s</DataType>
</column>
<column id="72" parent="9" name="delete_watched">
<Position>3</Position>
<DataType>bool|0s</DataType>
</column>
<column id="73" parent="9" name="auto_download">
<Position>4</Position>
<DataType>bool|0s</DataType>
</column>
<column id="74" parent="9" name="download_global_limit">
<Position>5</Position>
<DataType>integer|0s</DataType>
</column>
<column id="75" parent="9" name="download_subscription_limit">
<Position>6</Position>
<DataType>integer|0s</DataType>
</column>
<column id="76" parent="9" name="download_order">
<Position>7</Position>
<DataType>text|0s</DataType>
</column>
<column id="77" parent="9" name="download_path">
<Position>8</Position>
<DataType>text|0s</DataType>
</column>
<column id="78" parent="9" name="download_file_pattern">
<Position>9</Position>
<DataType>text|0s</DataType>
</column>
<column id="79" parent="9" name="download_format">
<Position>10</Position>
<DataType>text|0s</DataType>
</column>
<column id="80" parent="9" name="download_subtitles">
<Position>11</Position>
<DataType>bool|0s</DataType>
</column>
<column id="81" parent="9" name="download_autogenerated_subtitles">
<Position>12</Position>
<DataType>bool|0s</DataType>
</column>
<column id="82" parent="9" name="download_subtitles_all">
<Position>13</Position>
<DataType>bool|0s</DataType>
</column>
<column id="83" parent="9" name="download_subtitles_langs">
<Position>14</Position>
<DataType>text|0s</DataType>
</column>
<column id="84" parent="9" name="download_subtitles_format">
<Position>15</Position>
<DataType>text|0s</DataType>
</column>
<column id="85" parent="9" name="user_id">
<Position>16</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="86" parent="9" name="sqlite_autoindex_YtManagerApp_usersettings_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="87" parent="9">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="88" parent="9">
<ColNames>user_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_usersettings_1</UnderlyingIndexName>
</key>
<foreign-key id="89" parent="9">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="90" parent="10" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="91" parent="10" name="video_id">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="92" parent="10" name="name">
<Position>3</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="93" parent="10" name="description">
<Position>4</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="94" parent="10" name="watched">
<Position>5</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="95" parent="10" name="downloaded_path">
<Position>6</Position>
<DataType>text|0s</DataType>
</column>
<column id="96" parent="10" name="playlist_index">
<Position>7</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="97" parent="10" name="publish_date">
<Position>8</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="98" parent="10" name="icon_default">
<Position>9</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="99" parent="10" name="icon_best">
<Position>10</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="100" parent="10" name="subscription_id">
<Position>11</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="101" parent="10" name="YtManagerApp_video_subscription_id_720d4227">
<ColNames>subscription_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="102" parent="10">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="103" parent="10">
<ColNames>subscription_id</ColNames>
<RefTableName>YtManagerApp_subscription</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="104" parent="11" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="105" parent="11" name="name">
<Position>2</Position>
<DataType>varchar(80)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="106" parent="11" name="sqlite_autoindex_auth_group_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="107" parent="11">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="108" parent="11">
<ColNames>name</ColNames>
<UnderlyingIndexName>sqlite_autoindex_auth_group_1</UnderlyingIndexName>
</key>
<column id="109" parent="12" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="110" parent="12" name="group_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="111" parent="12" name="permission_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="112" parent="12" name="auth_group_permissions_group_id_permission_id_0cd325b0_uniq">
<ColNames>group_id
permission_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="113" parent="12" name="auth_group_permissions_group_id_b120cbf9">
<ColNames>group_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="114" parent="12" name="auth_group_permissions_permission_id_84c5c92e">
<ColNames>permission_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="115" parent="12">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="116" parent="12">
<ColNames>group_id</ColNames>
<RefTableName>auth_group</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="117" parent="12">
<ColNames>permission_id</ColNames>
<RefTableName>auth_permission</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="118" parent="13" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="119" parent="13" name="content_type_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="120" parent="13" name="codename">
<Position>3</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="121" parent="13" name="name">
<Position>4</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="122" parent="13" name="auth_permission_content_type_id_codename_01ab375a_uniq">
<ColNames>content_type_id
codename</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="123" parent="13" name="auth_permission_content_type_id_2f476e4b">
<ColNames>content_type_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="124" parent="13">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="125" parent="13">
<ColNames>content_type_id</ColNames>
<RefTableName>django_content_type</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="126" parent="14" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="127" parent="14" name="password">
<Position>2</Position>
<DataType>varchar(128)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="128" parent="14" name="last_login">
<Position>3</Position>
<DataType>datetime|0s</DataType>
</column>
<column id="129" parent="14" name="is_superuser">
<Position>4</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="130" parent="14" name="username">
<Position>5</Position>
<DataType>varchar(150)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="131" parent="14" name="first_name">
<Position>6</Position>
<DataType>varchar(30)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="132" parent="14" name="email">
<Position>7</Position>
<DataType>varchar(254)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="133" parent="14" name="is_staff">
<Position>8</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="134" parent="14" name="is_active">
<Position>9</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="135" parent="14" name="date_joined">
<Position>10</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="136" parent="14" name="last_name">
<Position>11</Position>
<DataType>varchar(150)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="137" parent="14" name="sqlite_autoindex_auth_user_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>username</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="138" parent="14">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="139" parent="14">
<ColNames>username</ColNames>
<UnderlyingIndexName>sqlite_autoindex_auth_user_1</UnderlyingIndexName>
</key>
<column id="140" parent="15" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="141" parent="15" name="user_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="142" parent="15" name="group_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="143" parent="15" name="auth_user_groups_user_id_group_id_94350c0c_uniq">
<ColNames>user_id
group_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="144" parent="15" name="auth_user_groups_user_id_6a12ed8b">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="145" parent="15" name="auth_user_groups_group_id_97559544">
<ColNames>group_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="146" parent="15">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="147" parent="15">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="148" parent="15">
<ColNames>group_id</ColNames>
<RefTableName>auth_group</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="149" parent="16" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="150" parent="16" name="user_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="151" parent="16" name="permission_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="152" parent="16" name="auth_user_user_permissions_user_id_permission_id_14a6b632_uniq">
<ColNames>user_id
permission_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="153" parent="16" name="auth_user_user_permissions_user_id_a95ead1b">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="154" parent="16" name="auth_user_user_permissions_permission_id_1fbb5f2c">
<ColNames>permission_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="155" parent="16">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="156" parent="16">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="157" parent="16">
<ColNames>permission_id</ColNames>
<RefTableName>auth_permission</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="158" parent="17" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="159" parent="17" name="action_time">
<Position>2</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="160" parent="17" name="object_id">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="161" parent="17" name="object_repr">
<Position>4</Position>
<DataType>varchar(200)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="162" parent="17" name="change_message">
<Position>5</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="163" parent="17" name="content_type_id">
<Position>6</Position>
<DataType>integer|0s</DataType>
</column>
<column id="164" parent="17" name="user_id">
<Position>7</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="165" parent="17" name="action_flag">
<Position>8</Position>
<DataType>smallint unsigned|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="166" parent="17" name="django_admin_log_content_type_id_c4bce8eb">
<ColNames>content_type_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="167" parent="17" name="django_admin_log_user_id_c564eba6">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="168" parent="17">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="169" parent="17">
<ColNames>content_type_id</ColNames>
<RefTableName>django_content_type</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="170" parent="17">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="171" parent="18" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="172" parent="18" name="app_label">
<Position>2</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="173" parent="18" name="model">
<Position>3</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="174" parent="18" name="django_content_type_app_label_model_76bd3d3b_uniq">
<ColNames>app_label
model</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="175" parent="18">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<column id="176" parent="19" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="177" parent="19" name="app">
<Position>2</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="178" parent="19" name="name">
<Position>3</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="179" parent="19" name="applied">
<Position>4</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="180" parent="19">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<column id="181" parent="20" name="session_key">
<Position>1</Position>
<DataType>varchar(40)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="182" parent="20" name="session_data">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="183" parent="20" name="expire_date">
<Position>3</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="184" parent="20" name="sqlite_autoindex_django_session_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>session_key</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="185" parent="20" name="django_session_expire_date_a5c62663">
<ColNames>expire_date</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="186" parent="20">
<ColNames>session_key</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_django_session_1</UnderlyingIndexName>
</key>
<column id="187" parent="21" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="188" parent="21" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="189" parent="21" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="190" parent="21" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="191" parent="21" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="192" parent="22" name="name">
<Position>1</Position>
</column>
<column id="193" parent="22" name="seq">
<Position>2</Position>
</column> </column>
</database-model> </database-model>
</dataSource> </dataSource>

View File

@ -0,0 +1,883 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.11">
<root id="1">
<ServerVersion>3.25.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
<Visible>1</Visible>
</schema>
<collation id="3" parent="1" name="BINARY"/>
<collation id="4" parent="1" name="NOCASE"/>
<collation id="5" parent="1" name="RTRIM"/>
<table id="6" parent="2" name="YtManagerApp_channel"/>
<table id="7" parent="2" name="YtManagerApp_subscription"/>
<table id="8" parent="2" name="YtManagerApp_subscriptionfolder"/>
<table id="9" parent="2" name="YtManagerApp_usersettings"/>
<table id="10" parent="2" name="YtManagerApp_video"/>
<table id="11" parent="2" name="auth_group"/>
<table id="12" parent="2" name="auth_group_permissions"/>
<table id="13" parent="2" name="auth_permission"/>
<table id="14" parent="2" name="auth_user"/>
<table id="15" parent="2" name="auth_user_groups"/>
<table id="16" parent="2" name="auth_user_user_permissions"/>
<table id="17" parent="2" name="django_admin_log"/>
<table id="18" parent="2" name="django_content_type"/>
<table id="19" parent="2" name="django_migrations"/>
<table id="20" parent="2" name="django_session"/>
<table id="21" parent="2" name="sqlite_master">
<System>1</System>
</table>
<table id="22" parent="2" name="sqlite_sequence">
<System>1</System>
</table>
<column id="23" parent="6" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="24" parent="6" name="channel_id">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="25" parent="6" name="username">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="26" parent="6" name="custom_url">
<Position>4</Position>
<DataType>text|0s</DataType>
</column>
<column id="27" parent="6" name="name">
<Position>5</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="28" parent="6" name="description">
<Position>6</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="6" name="icon_default">
<Position>7</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="6" name="icon_best">
<Position>8</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="31" parent="6" name="upload_playlist_id">
<Position>9</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="32" parent="6" name="sqlite_autoindex_YtManagerApp_channel_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>channel_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="33" parent="6" name="sqlite_autoindex_YtManagerApp_channel_2">
<NameSurrogate>1</NameSurrogate>
<ColNames>username</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="34" parent="6" name="sqlite_autoindex_YtManagerApp_channel_3">
<NameSurrogate>1</NameSurrogate>
<ColNames>custom_url</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="35" parent="6">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="36" parent="6">
<ColNames>channel_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_1</UnderlyingIndexName>
</key>
<key id="37" parent="6">
<ColNames>username</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_2</UnderlyingIndexName>
</key>
<key id="38" parent="6">
<ColNames>custom_url</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_channel_3</UnderlyingIndexName>
</key>
<column id="39" parent="7" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="40" parent="7" name="name">
<Position>2</Position>
<DataType>varchar(1024)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="41" parent="7" name="playlist_id">
<Position>3</Position>
<DataType>varchar(128)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="42" parent="7" name="description">
<Position>4</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="43" parent="7" name="icon_default">
<Position>5</Position>
<DataType>varchar(1024)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="7" name="icon_best">
<Position>6</Position>
<DataType>varchar(1024)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="7" name="auto_download">
<Position>7</Position>
<DataType>bool|0s</DataType>
</column>
<column id="46" parent="7" name="download_limit">
<Position>8</Position>
<DataType>integer|0s</DataType>
</column>
<column id="47" parent="7" name="download_order">
<Position>9</Position>
<DataType>varchar(128)|0s</DataType>
</column>
<column id="48" parent="7" name="channel_id">
<Position>10</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="49" parent="7" name="parent_folder_id">
<Position>11</Position>
<DataType>integer|0s</DataType>
</column>
<column id="50" parent="7" name="user_id">
<Position>12</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="7" name="delete_after_watched">
<Position>13</Position>
<DataType>bool|0s</DataType>
</column>
<index id="52" parent="7" name="YtManagerApp_subscription_channel_id_b83c6f21">
<ColNames>channel_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="53" parent="7" name="YtManagerApp_subscription_parent_folder_id_c4c64c21">
<ColNames>parent_folder_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="54" parent="7" name="YtManagerApp_subscription_user_id_9d38617d">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="55" parent="7">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="56" parent="7">
<ColNames>channel_id</ColNames>
<RefTableName>YtManagerApp_channel</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="57" parent="7">
<ColNames>parent_folder_id</ColNames>
<RefTableName>YtManagerApp_subscriptionfolder</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="58" parent="7">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="59" parent="8" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="60" parent="8" name="user_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="61" parent="8" name="name">
<Position>3</Position>
<DataType>varchar(250)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="62" parent="8" name="parent_id">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<index id="63" parent="8" name="YtManagerApp_subscriptionfolder_user_id_6fb12da0">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="64" parent="8" name="YtManagerApp_subscriptionfolder_parent_id_bd5f4bc1">
<ColNames>parent_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="65" parent="8">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="66" parent="8">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="67" parent="8">
<ColNames>parent_id</ColNames>
<RefTableName>YtManagerApp_subscriptionfolder</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="68" parent="9" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="69" parent="9" name="delete_watched">
<Position>2</Position>
<DataType>bool|0s</DataType>
</column>
<column id="70" parent="9" name="auto_download">
<Position>3</Position>
<DataType>bool|0s</DataType>
</column>
<column id="71" parent="9" name="download_global_limit">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="72" parent="9" name="download_subscription_limit">
<Position>5</Position>
<DataType>integer|0s</DataType>
</column>
<column id="73" parent="9" name="download_order">
<Position>6</Position>
<DataType>varchar(100)|0s</DataType>
</column>
<column id="74" parent="9" name="download_path">
<Position>7</Position>
<DataType>varchar(1024)|0s</DataType>
</column>
<column id="75" parent="9" name="download_file_pattern">
<Position>8</Position>
<DataType>varchar(1024)|0s</DataType>
</column>
<column id="76" parent="9" name="download_format">
<Position>9</Position>
<DataType>varchar(256)|0s</DataType>
</column>
<column id="77" parent="9" name="download_subtitles">
<Position>10</Position>
<DataType>bool|0s</DataType>
</column>
<column id="78" parent="9" name="download_autogenerated_subtitles">
<Position>11</Position>
<DataType>bool|0s</DataType>
</column>
<column id="79" parent="9" name="download_subtitles_all">
<Position>12</Position>
<DataType>bool|0s</DataType>
</column>
<column id="80" parent="9" name="download_subtitles_langs">
<Position>13</Position>
<DataType>varchar(250)|0s</DataType>
</column>
<column id="81" parent="9" name="download_subtitles_format">
<Position>14</Position>
<DataType>varchar(100)|0s</DataType>
</column>
<column id="82" parent="9" name="user_id">
<Position>15</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="83" parent="9" name="mark_deleted_as_watched">
<Position>16</Position>
<DataType>bool|0s</DataType>
</column>
<index id="84" parent="9" name="sqlite_autoindex_YtManagerApp_usersettings_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="85" parent="9">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="86" parent="9">
<ColNames>user_id</ColNames>
<UnderlyingIndexName>sqlite_autoindex_YtManagerApp_usersettings_1</UnderlyingIndexName>
</key>
<foreign-key id="87" parent="9">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="88" parent="10" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="89" parent="10" name="video_id">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" parent="10" name="name">
<Position>3</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="91" parent="10" name="description">
<Position>4</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="92" parent="10" name="watched">
<Position>5</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="93" parent="10" name="downloaded_path">
<Position>6</Position>
<DataType>text|0s</DataType>
</column>
<column id="94" parent="10" name="playlist_index">
<Position>7</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="95" parent="10" name="publish_date">
<Position>8</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="96" parent="10" name="icon_default">
<Position>9</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="97" parent="10" name="icon_best">
<Position>10</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="98" parent="10" name="subscription_id">
<Position>11</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="99" parent="10" name="rating">
<Position>12</Position>
<DataType>real|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="100" parent="10" name="uploader_name">
<Position>13</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="101" parent="10" name="views">
<Position>14</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="102" parent="10" name="YtManagerApp_video_subscription_id_720d4227">
<ColNames>subscription_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="103" parent="10">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="104" parent="10">
<ColNames>subscription_id</ColNames>
<RefTableName>YtManagerApp_subscription</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="105" parent="11" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="106" parent="11" name="name">
<Position>2</Position>
<DataType>varchar(80)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="107" parent="11" name="sqlite_autoindex_auth_group_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="108" parent="11">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="109" parent="11">
<ColNames>name</ColNames>
<UnderlyingIndexName>sqlite_autoindex_auth_group_1</UnderlyingIndexName>
</key>
<column id="110" parent="12" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="111" parent="12" name="group_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="112" parent="12" name="permission_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="113" parent="12" name="auth_group_permissions_group_id_permission_id_0cd325b0_uniq">
<ColNames>group_id
permission_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="114" parent="12" name="auth_group_permissions_group_id_b120cbf9">
<ColNames>group_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="115" parent="12" name="auth_group_permissions_permission_id_84c5c92e">
<ColNames>permission_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="116" parent="12">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="117" parent="12">
<ColNames>group_id</ColNames>
<RefTableName>auth_group</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="118" parent="12">
<ColNames>permission_id</ColNames>
<RefTableName>auth_permission</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="119" parent="13" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="120" parent="13" name="content_type_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="121" parent="13" name="codename">
<Position>3</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="122" parent="13" name="name">
<Position>4</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="123" parent="13" name="auth_permission_content_type_id_codename_01ab375a_uniq">
<ColNames>content_type_id
codename</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="124" parent="13" name="auth_permission_content_type_id_2f476e4b">
<ColNames>content_type_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="125" parent="13">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="126" parent="13">
<ColNames>content_type_id</ColNames>
<RefTableName>django_content_type</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="127" parent="14" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="128" parent="14" name="password">
<Position>2</Position>
<DataType>varchar(128)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="129" parent="14" name="last_login">
<Position>3</Position>
<DataType>datetime|0s</DataType>
</column>
<column id="130" parent="14" name="is_superuser">
<Position>4</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="131" parent="14" name="username">
<Position>5</Position>
<DataType>varchar(150)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="132" parent="14" name="first_name">
<Position>6</Position>
<DataType>varchar(30)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="133" parent="14" name="email">
<Position>7</Position>
<DataType>varchar(254)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="134" parent="14" name="is_staff">
<Position>8</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="135" parent="14" name="is_active">
<Position>9</Position>
<DataType>bool|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="136" parent="14" name="date_joined">
<Position>10</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="137" parent="14" name="last_name">
<Position>11</Position>
<DataType>varchar(150)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="138" parent="14" name="sqlite_autoindex_auth_user_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>username</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="139" parent="14">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<key id="140" parent="14">
<ColNames>username</ColNames>
<UnderlyingIndexName>sqlite_autoindex_auth_user_1</UnderlyingIndexName>
</key>
<column id="141" parent="15" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="142" parent="15" name="user_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="143" parent="15" name="group_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="144" parent="15" name="auth_user_groups_user_id_group_id_94350c0c_uniq">
<ColNames>user_id
group_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="145" parent="15" name="auth_user_groups_user_id_6a12ed8b">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="146" parent="15" name="auth_user_groups_group_id_97559544">
<ColNames>group_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="147" parent="15">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="148" parent="15">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="149" parent="15">
<ColNames>group_id</ColNames>
<RefTableName>auth_group</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="150" parent="16" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="151" parent="16" name="user_id">
<Position>2</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="152" parent="16" name="permission_id">
<Position>3</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="153" parent="16" name="auth_user_user_permissions_user_id_permission_id_14a6b632_uniq">
<ColNames>user_id
permission_id</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="154" parent="16" name="auth_user_user_permissions_user_id_a95ead1b">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="155" parent="16" name="auth_user_user_permissions_permission_id_1fbb5f2c">
<ColNames>permission_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="156" parent="16">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="157" parent="16">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="158" parent="16">
<ColNames>permission_id</ColNames>
<RefTableName>auth_permission</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="159" parent="17" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="160" parent="17" name="action_time">
<Position>2</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="161" parent="17" name="object_id">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="162" parent="17" name="object_repr">
<Position>4</Position>
<DataType>varchar(200)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="163" parent="17" name="change_message">
<Position>5</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="164" parent="17" name="content_type_id">
<Position>6</Position>
<DataType>integer|0s</DataType>
</column>
<column id="165" parent="17" name="user_id">
<Position>7</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="166" parent="17" name="action_flag">
<Position>8</Position>
<DataType>smallint unsigned|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="167" parent="17" name="django_admin_log_content_type_id_c4bce8eb">
<ColNames>content_type_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="168" parent="17" name="django_admin_log_user_id_c564eba6">
<ColNames>user_id</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="169" parent="17">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="170" parent="17">
<ColNames>content_type_id</ColNames>
<RefTableName>django_content_type</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<foreign-key id="171" parent="17">
<ColNames>user_id</ColNames>
<RefTableName>auth_user</RefTableName>
<RefColNames>id</RefColNames>
<Deferrable>1</Deferrable>
<InitiallyDeferred>1</InitiallyDeferred>
</foreign-key>
<column id="172" parent="18" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="173" parent="18" name="app_label">
<Position>2</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="174" parent="18" name="model">
<Position>3</Position>
<DataType>varchar(100)|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="175" parent="18" name="django_content_type_app_label_model_76bd3d3b_uniq">
<ColNames>app_label
model</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="176" parent="18">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<column id="177" parent="19" name="id">
<Position>1</Position>
<DataType>integer|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="178" parent="19" name="app">
<Position>2</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="179" parent="19" name="name">
<Position>3</Position>
<DataType>varchar(255)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="180" parent="19" name="applied">
<Position>4</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="181" parent="19">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<column id="182" parent="20" name="session_key">
<Position>1</Position>
<DataType>varchar(40)|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="183" parent="20" name="session_data">
<Position>2</Position>
<DataType>text|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="184" parent="20" name="expire_date">
<Position>3</Position>
<DataType>datetime|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="185" parent="20" name="sqlite_autoindex_django_session_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>session_key</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="186" parent="20" name="django_session_expire_date_a5c62663">
<ColNames>expire_date</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="187" parent="20">
<ColNames>session_key</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_django_session_1</UnderlyingIndexName>
</key>
<column id="188" parent="21" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="189" parent="21" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="190" parent="21" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="191" parent="21" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
</column>
<column id="192" parent="21" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="193" parent="22" name="name">
<Position>1</Position>
</column>
<column id="194" parent="22" name="seq">
<Position>2</Position>
</column>
</database-model>
</dataSource>

View File

@ -0,0 +1,2 @@
#n:main
!<md> [0, 0, null, null, -2147483648, -2147483648]

565
.idea/workspace.xml generated
View File

@ -2,10 +2,39 @@
<project version="4"> <project version="4">
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="245751b6-c863-4572-8723-8499964fe105" name="Default Changelist" comment=""> <list default="true" id="245751b6-c863-4572-8723-8499964fe105" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/readme.md" afterDir="false" /> <change afterPath="$PROJECT_DIR$/YtManagerApp/migrations/0007_auto_20181029_1638.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/.pytaw.conf" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/docs/Makefile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/docs/conf.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/docs/index.rst" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/docs/make.bat" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/main_test.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/pytaw/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/pytaw/utils.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/pytaw/youtube.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/setup.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/tests/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/external/pytaw/tests/test_pytaw.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources.local.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources.local.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources/2dac2136-d902-4d27-8789-9371934602fd.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/youtube-channel-manager.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/youtube-channel-manager.iml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/management/management.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/management/videos.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/management/videos.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/scheduler.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/scheduler.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/utils/iterutils.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/utils/youtube.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/utils/youtube.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/YtManagerApp/views/index.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/views/index.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/YtManagerApp/views/index.py" beforeDir="false" afterPath="$PROJECT_DIR$/YtManagerApp/views/index.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/config.ini" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.ini" afterDir="false" />
<change beforePath="$PROJECT_DIR$/readme.md" beforeDir="false" afterPath="$PROJECT_DIR$/readme.md" afterDir="false" />
</list> </list>
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" /> <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@ -32,19 +61,23 @@
<expand> <expand>
<path> <path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" /> <item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="Django default" type="feb32156:DbDataSourceImpl" /> <item name="db" type="feb32156:DbDataSourceImpl" />
</path> </path>
<path> <path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" /> <item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="Django default" type="feb32156:DbDataSourceImpl" /> <item name="db" type="feb32156:DbDataSourceImpl" />
<item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" /> <item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" />
</path> </path>
<path> <path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" /> <item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="Django default" type="feb32156:DbDataSourceImpl" /> <item name="db" type="feb32156:DbDataSourceImpl" />
<item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" /> <item name="schemas" type="d4e8921:DatabaseStructure$FamilyGroup" />
<item name="main: schema" type="90513b60:SqliteImplModel$Schema" /> <item name="main: schema" type="90513b60:SqliteImplModel$Schema" />
</path> </path>
<path>
<item name="Database" type="3277223f:DatabaseStructure$DbRootGroup" />
<item name="Django default" type="feb32156:DbDataSourceImpl" />
</path>
</expand> </expand>
<select /> <select />
</component> </component>
@ -137,51 +170,60 @@
<usages-collector id="statistics.lifecycle.project"> <usages-collector id="statistics.lifecycle.project">
<counts> <counts>
<entry key="project.open.time.10" value="1" /> <entry key="project.open.time.10" value="1" />
<entry key="project.opened" value="1" /> <entry key="project.open.time.13" value="1" />
<entry key="project.opened" value="2" />
</counts> </counts>
</usages-collector> </usages-collector>
<usages-collector id="statistics.file.extensions.open"> <usages-collector id="statistics.file.extensions.open">
<counts> <counts>
<entry key="auth_group" value="1" />
<entry key="gitignore" value="1" /> <entry key="gitignore" value="1" />
<entry key="html" value="1" /> <entry key="html" value="1" />
<entry key="md" value="2" /> <entry key="ini" value="2" />
<entry key="py" value="3" /> <entry key="md" value="5" />
<entry key="py" value="35" />
<entry key="ytmanagerapp_channel" value="1" />
<entry key="ytmanagerapp_subscription" value="2" />
<entry key="ytmanagerapp_video" value="3" />
</counts> </counts>
</usages-collector> </usages-collector>
<usages-collector id="statistics.file.types.open"> <usages-collector id="statistics.file.types.open">
<counts> <counts>
<entry key="Database Element" value="7" />
<entry key="HTML" value="1" /> <entry key="HTML" value="1" />
<entry key="Markdown" value="2" /> <entry key="Ini" value="2" />
<entry key="Markdown" value="5" />
<entry key="PLAIN_TEXT" value="1" /> <entry key="PLAIN_TEXT" value="1" />
<entry key="Python" value="3" /> <entry key="Python" value="35" />
</counts> </counts>
</usages-collector> </usages-collector>
<usages-collector id="statistics.file.extensions.edit"> <usages-collector id="statistics.file.extensions.edit">
<counts> <counts>
<entry key="md" value="464" /> <entry key="Django Console" value="155" />
<entry key="py" value="1" /> <entry key="ini" value="2" />
<entry key="py@youtube-channel-manager" value="12" /> <entry key="md" value="509" />
<entry key="py" value="3244" />
<entry key="py@youtube-channel-manager" value="41" />
</counts> </counts>
</usages-collector> </usages-collector>
<usages-collector id="statistics.file.types.edit"> <usages-collector id="statistics.file.types.edit">
<counts> <counts>
<entry key="CommandLine" value="12" /> <entry key="CommandLine" value="23" />
<entry key="Markdown" value="464" /> <entry key="Ini" value="2" />
<entry key="Python" value="1" /> <entry key="Markdown" value="509" />
<entry key="PLAIN_TEXT" value="18" />
<entry key="Python" value="3399" />
</counts> </counts>
</usages-collector> </usages-collector>
</session> </session>
</component> </component>
<component name="FileEditorManager"> <component name="FileEditorManager">
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300"> <leaf>
<file pinned="false" current-in-tab="true"> <file pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/readme.md"> <entry file="file://$PROJECT_DIR$/config/config.ini">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]"> <provider selected="true" editor-type-id="text-editor">
<state split_layout="SPLIT"> <state relative-caret-position="373">
<first_editor relative-caret-position="180"> <caret line="44" lean-forward="true" selection-start-line="44" selection-end-line="44" />
<caret line="12" column="31" lean-forward="true" selection-start-line="12" selection-start-column="31" selection-end-line="12" selection-end-column="31" />
</first_editor>
<second_editor />
</state> </state>
</provider> </provider>
</entry> </entry>
@ -194,22 +236,13 @@
<option value="SCSS File" /> <option value="SCSS File" />
<option value="Setup Script" /> <option value="Setup Script" />
<option value="JavaScript File" /> <option value="JavaScript File" />
<option value="Python Script" />
<option value="HTML File" /> <option value="HTML File" />
<option value="Python Script" />
</list> </list>
</option> </option>
</component> </component>
<component name="FindInProjectRecents"> <component name="FindInProjectRecents">
<findStrings> <findStrings>
<find>ConfigParserWithEnv</find>
<find>logger</find>
<find>app_config</find>
<find>dialog_</find>
<find>.find</find>
<find>.show</find>
<find>videos_wrapper</find>
<find>videos_loading</find>
<find>submit</find>
<find>modal_edit_folder</find> <find>modal_edit_folder</find>
<find>modal_update_folder</find> <find>modal_update_folder</find>
<find>modal_delete_folder</find> <find>modal_delete_folder</find>
@ -231,6 +264,15 @@
<find>settings</find> <find>settings</find>
<find>settings.</find> <find>settings.</find>
<find>manager_delete_after_watched</find> <find>manager_delete_after_watched</find>
<find>dist</find>
<find>relatedPlaylists</find>
<find>thumbnails</find>
<find>'thumbnails'</find>
<find>thumbn</find>
<find>thumbnail</find>
<find>get_or_create</find>
<find>video</find>
<find>class thum</find>
</findStrings> </findStrings>
<replaceStrings> <replaceStrings>
<replace>loading</replace> <replace>loading</replace>
@ -254,8 +296,6 @@
<component name="IdeDocumentHistory"> <component name="IdeDocumentHistory">
<option name="CHANGED_PATHS"> <option name="CHANGED_PATHS">
<list> <list>
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/controls/folder_create_modal.html" />
<option value="$PROJECT_DIR$/../YoutubeApi-tests/multiinheritance.py" />
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/controls/folder_edit_modal.html" /> <option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/controls/folder_edit_modal.html" />
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html" /> <option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html" />
<option value="$PROJECT_DIR$/YtManagerApp/management/folders.py" /> <option value="$PROJECT_DIR$/YtManagerApp/management/folders.py" />
@ -277,11 +317,7 @@
<option value="$PROJECT_DIR$/YtManagerApp/apps.py" /> <option value="$PROJECT_DIR$/YtManagerApp/apps.py" />
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/js/index.js" /> <option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/js/index.js" />
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/js/common.js" /> <option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/js/common.js" />
<option value="$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py" />
<option value="$PROJECT_DIR$/YtManagerApp/scheduler.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/videos.py" />
<option value="$PROJECT_DIR$/YtManagerApp/utils/iterutils.py" /> <option value="$PROJECT_DIR$/YtManagerApp/utils/iterutils.py" />
<option value="$PROJECT_DIR$/YtManagerApp/utils/youtube.py" />
<option value="$PROJECT_DIR$/YtManagerApp/templatetags/common.py" /> <option value="$PROJECT_DIR$/YtManagerApp/templatetags/common.py" />
<option value="$PROJECT_DIR$/YtManagerApp/templatetags/ratings.py" /> <option value="$PROJECT_DIR$/YtManagerApp/templatetags/ratings.py" />
<option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/index_videos.html" /> <option value="$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/index_videos.html" />
@ -297,14 +333,20 @@
<option value="$PROJECT_DIR$/YtManagerApp/utils/customconfigparser.py" /> <option value="$PROJECT_DIR$/YtManagerApp/utils/customconfigparser.py" />
<option value="$PROJECT_DIR$/YtManagerApp/utils/extended_interpolation_with_env.py" /> <option value="$PROJECT_DIR$/YtManagerApp/utils/extended_interpolation_with_env.py" />
<option value="$PROJECT_DIR$/config/defaults.ini" /> <option value="$PROJECT_DIR$/config/defaults.ini" />
<option value="$PROJECT_DIR$/YtManagerApp/models.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py" />
<option value="$PROJECT_DIR$/YtManagerApp/appconfig.py" /> <option value="$PROJECT_DIR$/YtManagerApp/appconfig.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/downloader.py" /> <option value="$PROJECT_DIR$/YtManagerApp/management/downloader.py" />
<option value="$PROJECT_DIR$/config/config.ini" /> <option value="$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py" />
<option value="$PROJECT_DIR$/YtManagerApp/views/index.py" /> <option value="$PROJECT_DIR$/YtManagerApp/scheduler.py" />
<option value="$PROJECT_DIR$/readme.md" /> <option value="$PROJECT_DIR$/readme.md" />
<option value="$PROJECT_DIR$/external/pytaw/README.md" />
<option value="$PROJECT_DIR$/external/pytaw/pytaw/youtube.py" />
<option value="$PROJECT_DIR$/YtManagerApp/utils/youtube.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/videos.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py" />
<option value="$PROJECT_DIR$/YtManagerApp/models.py" />
<option value="$PROJECT_DIR$/YtManagerApp/views/index.py" />
<option value="$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py" />
<option value="$PROJECT_DIR$/config/config.ini" />
</list> </list>
</option> </option>
</component> </component>
@ -320,8 +362,8 @@
</packageJsonPaths> </packageJsonPaths>
</component> </component>
<component name="ProjectFrameBounds" extendedState="6"> <component name="ProjectFrameBounds" extendedState="6">
<option name="x" value="-8" /> <option name="x" value="803" />
<option name="y" value="-8" /> <option name="y" value="615" />
<option name="width" value="643" /> <option name="width" value="643" />
<option name="height" value="321" /> <option name="height" value="321" />
</component> </component>
@ -378,6 +420,7 @@
<foldersAlwaysOnTop value="true" /> <foldersAlwaysOnTop value="true" />
</navigator> </navigator>
<panes> <panes>
<pane id="Scope" />
<pane id="ProjectPane"> <pane id="ProjectPane">
<subPane> <subPane>
<expand> <expand>
@ -385,6 +428,11 @@
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" /> <item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" /> <item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
</path> </path>
<path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="config" type="462c0819:PsiDirectoryNode" />
</path>
<path> <path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" /> <item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" /> <item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
@ -394,50 +442,25 @@
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" /> <item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" /> <item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" /> <item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="static" type="462c0819:PsiDirectoryNode" /> <item name="management" type="462c0819:PsiDirectoryNode" />
</path> </path>
<path> <path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" /> <item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" /> <item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" /> <item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="static" type="462c0819:PsiDirectoryNode" /> <item name="management" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" /> <item name="jobs" type="462c0819:PsiDirectoryNode" />
</path> </path>
<path> <path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" /> <item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" /> <item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" /> <item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="static" type="462c0819:PsiDirectoryNode" /> <item name="utils" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="import" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="static" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="import" type="462c0819:PsiDirectoryNode" />
<item name="bootstrap-4.1.3" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="templates" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="youtube-channel-manager" type="b2602c69:ProjectViewProjectNode" />
<item name="youtube-channel-manager" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
<item name="templates" type="462c0819:PsiDirectoryNode" />
<item name="YtManagerApp" type="462c0819:PsiDirectoryNode" />
</path> </path>
</expand> </expand>
<select /> <select />
</subPane> </subPane>
</pane> </pane>
<pane id="Scope" />
</panes> </panes>
</component> </component>
<component name="PropertiesComponent"> <component name="PropertiesComponent">
@ -445,26 +468,26 @@
<property name="SearchEverywhereHistoryKey" value="&#9;FILE&#9;file://D:/Dev/youtube-channel-manager/YtManager/settings.py" /> <property name="SearchEverywhereHistoryKey" value="&#9;FILE&#9;file://D:/Dev/youtube-channel-manager/YtManager/settings.py" />
<property name="WebServerToolWindowFactoryState" value="false" /> <property name="WebServerToolWindowFactoryState" value="false" />
<property name="database.console.LAST_STATE" value="false" /> <property name="database.console.LAST_STATE" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/../YoutubeApi-tests/get_playlist_info.py" /> <property name="last_opened_file_path" value="$PROJECT_DIR$/../pytaw" />
<property name="list.type.of.created.stylesheet" value="SCSS" /> <property name="list.type.of.created.stylesheet" value="SCSS" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" /> <property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" /> <property name="nodejs_npm_path_reset_for_default_project" value="true" />
<property name="settings.editor.selected.configurable" value="reference.settings.ide.settings.web.browsers" /> <property name="settings.editor.selected.configurable" value="reference.settings.ide.settings.web.browsers" />
</component> </component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\management\jobs" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp\controls" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\registration" />
<recent name="D:\Dev\youtube-channel-manager\config" />
</key>
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp\js" /> <recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp\js" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\views\controls" /> <recent name="D:\Dev\youtube-channel-manager\YtManagerApp\views\controls" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\views" /> <recent name="D:\Dev\youtube-channel-manager\YtManagerApp\views" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp" /> <recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\static\YtManagerApp" /> </key>
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/YtManagerApp/import" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\management\jobs" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\YtManagerApp\controls" />
<recent name="D:\Dev\youtube-channel-manager\YtManagerApp\templates\registration" />
</key> </key>
</component> </component>
<component name="RunDashboard"> <component name="RunDashboard">
@ -610,29 +633,29 @@
<frame x="0" y="0" width="1920" height="1048" extended-state="6" /> <frame x="0" y="0" width="1920" height="1048" extended-state="6" />
<editor active="true" /> <editor active="true" />
<layout> <layout>
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.18603411" /> <window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.18923241" />
<window_info id="Structure" order="1" side_tool="true" weight="0.25" /> <window_info id="Structure" order="1" side_tool="true" weight="0.25" />
<window_info id="Favorites" order="2" side_tool="true" /> <window_info id="Favorites" order="2" side_tool="true" />
<window_info anchor="bottom" id="Message" order="0" /> <window_info anchor="bottom" id="Message" order="0" />
<window_info anchor="bottom" id="Find" order="1" sideWeight="0.49733475" weight="0.329718" /> <window_info anchor="bottom" id="Find" order="1" sideWeight="0.49520257" weight="0.38152173" />
<window_info anchor="bottom" id="Run" order="2" sideWeight="0.5245203" visible="true" weight="0.33913043" /> <window_info anchor="bottom" id="Run" order="2" sideWeight="0.5234541" visible="true" weight="0.2228261" />
<window_info anchor="bottom" id="Cvs" order="3" weight="0.25" /> <window_info anchor="bottom" id="Cvs" order="3" weight="0.25" />
<window_info anchor="bottom" id="Inspection" order="4" weight="0.4" /> <window_info anchor="bottom" id="Inspection" order="4" weight="0.4" />
<window_info anchor="bottom" id="Debug" order="5" sideWeight="0.49520257" weight="0.37608695" /> <window_info anchor="bottom" id="Debug" order="5" sideWeight="0.4936034" weight="0.37608695" />
<window_info anchor="bottom" id="TODO" order="6" weight="0.329718" /> <window_info anchor="bottom" id="TODO" order="6" weight="0.329718" />
<window_info anchor="bottom" id="manage.py@youtube-channel-manager" order="7" sideWeight="0.49626866" weight="0.49347827" /> <window_info anchor="bottom" id="manage.py@youtube-channel-manager" order="7" sideWeight="0.49573562" weight="0.49347827" />
<window_info anchor="bottom" id="Docker" order="8" show_stripe_button="false" /> <window_info anchor="bottom" id="Docker" order="8" show_stripe_button="false" />
<window_info anchor="bottom" id="Database Changes" order="9" weight="0.3285968" /> <window_info anchor="bottom" id="Database Changes" order="9" weight="0.3285968" />
<window_info anchor="bottom" id="Event Log" order="10" sideWeight="0.47547975" side_tool="true" visible="true" weight="0.33913043" /> <window_info anchor="bottom" id="Event Log" order="10" sideWeight="0.47654584" side_tool="true" visible="true" weight="0.2228261" />
<window_info anchor="bottom" id="Version Control" order="11" sideWeight="0.49946696" weight="0.329718" /> <window_info anchor="bottom" id="Version Control" order="11" sideWeight="0.49946696" weight="0.329718" />
<window_info anchor="bottom" id="Terminal" order="12" sideWeight="0.49626866" weight="0.3576087" /> <window_info anchor="bottom" id="Terminal" order="12" sideWeight="0.49573562" weight="0.3576087" />
<window_info anchor="bottom" id="Python Console" order="13" sideWeight="0.49733475" weight="0.3347826" /> <window_info anchor="bottom" id="Python Console" order="13" sideWeight="0.4968017" weight="0.33804348" />
<window_info anchor="bottom" id="Database Console" order="14" sideWeight="0.49838188" weight="0.21908894" /> <window_info anchor="bottom" id="Database Console" order="14" sideWeight="0.49838188" weight="0.21908894" />
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" /> <window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" /> <window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" /> <window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
<window_info anchor="right" id="SciView" order="3" weight="0.32995737" /> <window_info anchor="right" id="SciView" order="3" weight="0.32995737" />
<window_info anchor="right" id="Database" order="4" weight="0.2675906" /> <window_info anchor="right" id="Database" order="4" visible="true" weight="0.20469083" />
</layout> </layout>
<layout-to-restore> <layout-to-restore>
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.15138593" /> <window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.15138593" />
@ -667,75 +690,6 @@
<option name="myLimit" value="2678400000" /> <option name="myLimit" value="2678400000" />
</component> </component>
<component name="editorHistoryManager"> <component name="editorHistoryManager">
<entry file="file://$PROJECT_DIR$/YtManagerApp/utils/youtube.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="11" lean-forward="true" selection-start-line="11" selection-end-line="11" />
<folding>
<element signature="e#0#43#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/videos.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="357">
<caret line="24" column="9" lean-forward="true" selection-start-line="24" selection-start-column="9" selection-end-line="24" selection-end-column="9" />
<folding>
<element signature="e#0#71#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="68">
<caret line="4" selection-start-line="4" selection-end-line="4" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/templatetags/common.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="459">
<caret line="30" lean-forward="true" selection-start-line="30" selection-end-line="30" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/templatetags/ratings.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="30" column="9" lean-forward="true" selection-start-line="30" selection-start-column="9" selection-end-line="30" selection-end-column="9" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/views/old_views.py" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/index.html">
<provider selected="true" editor-type-id="text-editor">
<state>
<caret selection-end-column="48" />
</state>
</provider>
</entry>
<entry file="file://C:/Python36/Lib/site-packages/django/template/base.py" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/index_videos.html">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="143">
<caret line="23" column="56" selection-start-line="23" selection-start-column="56" selection-end-line="23" selection-end-column="56" />
</state>
</provider>
</entry>
<entry file="file://C:/Python36/Lib/site-packages/django/core/handlers/exception.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/views/generic/base.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/views/generic/edit.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/db/models/query.py" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/server_settings.html" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/templates/YtManagerApp/settings.html">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="11" column="18" lean-forward="true" selection-start-line="11" selection-start-column="18" selection-end-line="11" selection-end-column="18" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/static/YtManagerApp/css/style.scss"> <entry file="file://$PROJECT_DIR$/YtManagerApp/static/YtManagerApp/css/style.scss">
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="391"> <state relative-caret-position="391">
@ -751,20 +705,6 @@
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="das://2dac2136-d902-4d27-8789-9371934602fd/schema/main/table/ytmanagerapp_video">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="das://2dac2136-d902-4d27-8789-9371934602fd/schema/main/table/ytmanagerapp_subscription">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="file://C:/Python36/Lib/site-packages/django/db/models/fields/__init__.py" /> <entry file="file://C:/Python36/Lib/site-packages/django/db/models/fields/__init__.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/forms/models.py" /> <entry file="file://C:/Python36/Lib/site-packages/django/forms/models.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/contrib/auth/decorators.py" /> <entry file="file://C:/Python36/Lib/site-packages/django/contrib/auth/decorators.py" />
@ -772,9 +712,6 @@
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="136"> <state relative-caret-position="136">
<caret line="8" lean-forward="true" selection-start-line="8" selection-end-line="8" /> <caret line="8" lean-forward="true" selection-start-line="8" selection-end-line="8" />
<folding>
<element signature="e#0#42#0" expanded="true" />
</folding>
</state> </state>
</provider> </provider>
</entry> </entry>
@ -797,48 +734,15 @@
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="459"> <state relative-caret-position="459">
<caret line="92" lean-forward="true" selection-start-line="92" selection-end-line="92" /> <caret line="92" lean-forward="true" selection-start-line="92" selection-end-line="92" />
<folding>
<element signature="e#0#9#0" expanded="true" />
</folding>
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="file://C:/Python36/Lib/collections/__init__.py" /> <entry file="file://C:/Python36/Lib/collections/__init__.py" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="510">
<caret line="102" column="12" lean-forward="true" selection-start-line="102" selection-start-column="12" selection-end-line="102" selection-end-column="12" />
<folding>
<element signature="e#0#37#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/scheduler.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="136">
<caret line="8" column="32" lean-forward="true" selection-start-line="8" selection-start-column="32" selection-end-line="8" selection-end-column="32" />
<folding>
<element signature="e#0#14#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/.PyCharm2018.2/system/python_stubs/-1184660488/builtins.py" /> <entry file="file://$USER_HOME$/.PyCharm2018.2/system/python_stubs/-1184660488/builtins.py" />
<entry file="file://$PROJECT_DIR$/YtManagerApp/urls.py"> <entry file="file://$PROJECT_DIR$/YtManagerApp/urls.py">
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="408"> <state relative-caret-position="408">
<caret line="33" selection-start-line="33" selection-end-line="33" /> <caret line="33" selection-start-line="33" selection-end-line="33" />
<folding>
<element signature="e#643#675#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManager/settings.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="746">
<caret line="132" selection-start-line="132" selection-end-line="132" />
</state> </state>
</provider> </provider>
</entry> </entry>
@ -856,29 +760,13 @@
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187"> <state relative-caret-position="187">
<caret line="11" selection-start-line="11" selection-end-line="11" /> <caret line="11" selection-start-line="11" selection-end-line="11" />
<folding>
<element signature="e#0#42#0" expanded="true" />
</folding>
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="file://C:/Python36/Lib/configparser.py" /> <entry file="file://C:/Python36/Lib/configparser.py" />
<entry file="file://C:/Python36/Lib/site-packages/apscheduler/executors/base.py" /> <entry file="file://C:/Python36/Lib/site-packages/apscheduler/executors/base.py" />
<entry file="file://C:/Python36/Lib/site-packages/django/db/models/fields/related_descriptors.py" /> <entry file="file://C:/Python36/Lib/site-packages/django/db/models/fields/related_descriptors.py" />
<entry file="file:///usr/lib/python3/dist-packages/django/db/models/base.py"> <entry file="file:///usr/lib/python3/dist-packages/django/db/models/base.py" />
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="193">
<caret line="1652" selection-start-line="1652" selection-end-line="1652" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/views/index.py">
<provider selected="true" editor-type-id="text-editor">
<state>
<caret column="49" selection-start-column="49" selection-end-column="49" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/appmain.py"> <entry file="file://$PROJECT_DIR$/YtManagerApp/appmain.py">
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="165"> <state relative-caret-position="165">
@ -916,30 +804,6 @@
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="311">
<caret line="123" selection-start-line="123" selection-end-line="123" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/config.ini">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="491">
<caret line="59" selection-start-line="59" selection-end-line="59" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/models.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="345">
<caret line="23" column="30" lean-forward="true" selection-start-line="23" selection-start-column="30" selection-end-line="23" selection-end-column="30" />
<folding>
<element signature="e#0#14#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/static/YtManagerApp/import/bootstrap-4.1.3/.gitignore"> <entry file="file://$PROJECT_DIR$/YtManagerApp/static/YtManagerApp/import/bootstrap-4.1.3/.gitignore">
<provider selected="true" editor-type-id="text-editor"> <provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="45"> <state relative-caret-position="45">
@ -954,15 +818,204 @@
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/delete_video.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="485">
<caret line="37" column="65" selection-start-line="37" selection-start-column="65" selection-end-line="38" selection-end-column="76" />
<folding>
<element signature="e#0#14#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/local/lib/python3.6/dist-packages/apscheduler/schedulers/base.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="421">
<caret line="440" column="16" selection-start-line="440" selection-start-column="16" selection-end-line="440" selection-end-column="16" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/readme.md"> <entry file="file://$PROJECT_DIR$/readme.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]"> <provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT"> <state split_layout="SPLIT">
<first_editor relative-caret-position="180"> <first_editor relative-caret-position="60">
<caret line="12" column="31" lean-forward="true" selection-start-line="12" selection-start-column="31" selection-end-line="12" selection-end-column="31" /> <caret line="4" selection-start-line="4" selection-end-line="4" />
</first_editor> </first_editor>
<second_editor /> <second_editor />
</state> </state>
</provider> </provider>
</entry> </entry>
<entry file="file://$PROJECT_DIR$/YtManager/settings.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1046">
<caret line="132" selection-start-line="132" selection-end-line="132" />
</state>
</provider>
</entry>
<entry file="das://2dac2136-d902-4d27-8789-9371934602fd/schema/main/table/ytmanagerapp_subscription">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="das://2dac2136-d902-4d27-8789-9371934602fd/schema/main/table/auth_group">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="das://2dac2136-d902-4d27-8789-9371934602fd/schema/main/table/ytmanagerapp_video">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/external/pytaw/docs/conf.py">
<provider selected="true" editor-type-id="text-editor" />
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/scheduler.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="300">
<caret line="20" column="5" selection-start-line="20" selection-start-column="5" selection-end-line="20" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/utils/iterutils.py" />
<entry file="das://77df9da5-0b97-445e-a895-744ef8257a74/schema/main/table/ytmanagerapp_channel">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="das://77df9da5-0b97-445e-a895-744ef8257a74/schema/main/table/ytmanagerapp_subscription">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="das://77df9da5-0b97-445e-a895-744ef8257a74/schema/main/table/ytmanagerapp_video">
<provider selected="true" editor-type-id="com.intellij.database.editor.DatabaseTableFileEditorProvider">
<state>
<filtering enabled="true" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/external/pytaw/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="75">
<caret line="5" lean-forward="true" selection-start-line="5" selection-end-line="5" />
</first_editor>
<second_editor />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/management.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1500">
<caret line="100" column="20" lean-forward="true" selection-start-line="100" selection-start-column="20" selection-end-line="100" selection-end-column="20" />
<folding>
<element signature="e#0#65#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/videos.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-308">
<caret line="8" lean-forward="true" selection-start-line="8" selection-end-line="8" />
<folding>
<element signature="e#0#9#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/external/pytaw/pytaw/youtube.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="174" column="11" lean-forward="true" selection-start-line="174" selection-start-column="11" selection-end-line="174" selection-end-column="11" />
<folding>
<element signature="e#0#18#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/local/lib/python3.6/dist-packages/django/forms/utils.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="75">
<caret line="5" selection-start-line="5" selection-end-line="5" />
</state>
</provider>
</entry>
<entry file="file:///usr/local/lib/python3.6/dist-packages/django/db/backends/sqlite3/base.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="138">
<caret line="314" column="31" selection-start-line="314" selection-start-column="31" selection-end-line="314" selection-end-column="31" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/models.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="251">
<caret line="280" column="32" selection-start-line="280" selection-start-column="22" selection-end-line="280" selection-end-column="32" />
<folding>
<element signature="e#0#14#0" expanded="true" />
<marker date="1540831325280" expanded="true" signature="7713:7719" ph="..." />
<marker date="1540831325280" expanded="true" signature="13276:13861" ph="..." />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/utils/youtube.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="120">
<caret line="8" column="27" selection-start-line="8" selection-start-column="27" selection-end-line="8" selection-end-column="27" />
<folding>
<element signature="e#0#32#0" expanded="true" />
<marker date="1540827548686" expanded="true" signature="184:185" ph="..." />
<marker date="1540827548686" expanded="true" signature="761:766" ph="..." />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/synchronize.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="270">
<caret line="18" column="71" selection-start-line="18" selection-start-column="71" selection-end-line="18" selection-end-column="71" />
<folding>
<element signature="e#0#12#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/views/index.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="521">
<caret line="299" selection-start-line="299" selection-end-line="299" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/YtManagerApp/management/jobs/download_video.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="408">
<caret line="99" column="49" selection-start-line="99" selection-start-column="49" selection-end-line="99" selection-end-column="49" />
<folding>
<element signature="e#0#37#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/config/config.ini">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="373">
<caret line="44" lean-forward="true" selection-start-line="44" selection-end-line="44" />
</state>
</provider>
</entry>
</component> </component>
</project> </project>

View File

@ -35,4 +35,5 @@ def schedule_delete_video(video: Video):
:param video: :param video:
:return: :return:
""" """
scheduler.instance.add_job(delete_video, args=[video]) job = scheduler.scheduler.add_job(delete_video, args=[video])
log.info('Scheduled delete video job video=(%s), job=%s', video, job.id)

View File

@ -25,8 +25,8 @@ def __get_valid_path(path):
def __build_youtube_dl_params(video: Video): def __build_youtube_dl_params(video: Video):
# resolve path # resolve path
pattern_dict = { pattern_dict = {
'channel': video.subscription.channel.name, 'channel': video.subscription.channel_name,
'channel_id': video.subscription.channel.channel_id, 'channel_id': video.subscription.channel_id,
'playlist': video.subscription.name, 'playlist': video.subscription.name,
'playlist_id': video.subscription.playlist_id, 'playlist_id': video.subscription.playlist_id,
'playlist_index': "{:03d}".format(1 + video.playlist_index), 'playlist_index': "{:03d}".format(1 + video.playlist_index),
@ -88,7 +88,7 @@ def download_video(video: Video, attempt: int = 1):
elif attempt <= max_attempts: elif attempt <= max_attempts:
log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts) log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts)
scheduler.instance.add_job(download_video, args=[video, attempt + 1]) __schedule_download_video(video, attempt + 1)
else: else:
log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name) log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name)
@ -96,10 +96,15 @@ def download_video(video: Video, attempt: int = 1):
video.save() video.save()
def __schedule_download_video(video: Video, attempt=1):
job = scheduler.scheduler.add_job(download_video, args=[video, attempt])
log.info('Scheduled download video job video=(%s), attempt=%d, job=%s', video, attempt, job.id)
def schedule_download_video(video: Video): def schedule_download_video(video: Video):
""" """
Schedules a download video job to run immediately. Schedules a download video job to run immediately.
:param video: :param video:
:return: :return:
""" """
scheduler.instance.add_job(download_video, args=[video, 1]) __schedule_download_video(video)

View File

@ -7,9 +7,8 @@ from apscheduler.triggers.cron import CronTrigger
from YtManagerApp import scheduler from YtManagerApp import scheduler
from YtManagerApp.appconfig import settings from YtManagerApp.appconfig import settings
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription
from YtManagerApp.management.videos import create_video
from YtManagerApp.models import * from YtManagerApp.models import *
from YtManagerApp.utils.youtube import YoutubeAPI from YtManagerApp.utils import youtube
log = logging.getLogger('sync') log = logging.getLogger('sync')
__lock = Lock() __lock = Lock()
@ -17,23 +16,25 @@ __lock = Lock()
_ENABLE_UPDATE_STATS = False _ENABLE_UPDATE_STATS = False
def __check_new_videos_sub(subscription: Subscription, yt_api: YoutubeAPI): def __check_new_videos_sub(subscription: Subscription, yt_api: youtube.YoutubeAPI):
# Get list of videos # Get list of videos
for video in yt_api.list_playlist_videos(subscription.playlist_id): for item in yt_api.playlist_items(subscription.playlist_id):
results = Video.objects.filter(video_id=video.getVideoId(), subscription=subscription) results = Video.objects.filter(video_id=item.resource_video_id, subscription=subscription)
if len(results) == 0: if len(results) == 0:
log.info('New video for subscription %s: %s %s"', subscription, video.getVideoId(), video.getTitle()) log.info('New video for subscription %s: %s %s"', subscription, item.resource_video_id, item.title)
db_video = create_video(video, subscription) Video.create(item, subscription)
else:
if not _ENABLE_UPDATE_STATS:
continue
db_video = results.first()
# Update video stats - rating and view count if _ENABLE_UPDATE_STATS:
stats = yt_api.get_single_video_stats(db_video.video_id) all_vids = Video.objects.filter(subscription=subscription)
db_video.rating = stats.get_like_count() / (stats.get_like_count() + stats.get_dislike_count()) all_vids_ids = [video.video_id for video in all_vids]
db_video.views = stats.get_view_count() all_vids_dict = {v.video_id: v for v in all_vids}
db_video.save()
for yt_video in yt_api.videos(all_vids_ids, part='id,statistics'):
video = all_vids_dict.get(yt_video.id)
if yt_video.like_count is not None and yt_video.dislike_count is not None:
video.rating = yt_video.n_likes / (yt_video.n_likes + yt_video.n_dislikes)
video.views = yt_video.n_views
video.save()
def __detect_deleted(subscription: Subscription): def __detect_deleted(subscription: Subscription):
@ -82,11 +83,6 @@ def __fetch_thumbnails_obj(iterable, obj_type, id_attr):
def __fetch_thumbnails(): def __fetch_thumbnails():
# Fetch thumbnails
log.info("Fetching channel thumbnails... ")
__fetch_thumbnails_obj(Channel.objects.filter(icon_default__istartswith='http'), 'channel', 'channel_id')
__fetch_thumbnails_obj(Channel.objects.filter(icon_best__istartswith='http'), 'channel', 'channel_id')
log.info("Fetching subscription thumbnails... ") log.info("Fetching subscription thumbnails... ")
__fetch_thumbnails_obj(Subscription.objects.filter(icon_default__istartswith='http'), 'sub', 'playlist_id') __fetch_thumbnails_obj(Subscription.objects.filter(icon_default__istartswith='http'), 'sub', 'playlist_id')
__fetch_thumbnails_obj(Subscription.objects.filter(icon_best__istartswith='http'), 'sub', 'playlist_id') __fetch_thumbnails_obj(Subscription.objects.filter(icon_best__istartswith='http'), 'sub', 'playlist_id')
@ -107,7 +103,7 @@ def synchronize():
# Sync subscribed playlists/channels # Sync subscribed playlists/channels
log.info("Sync - checking videos") log.info("Sync - checking videos")
yt_api = YoutubeAPI.build_public() yt_api = youtube.YoutubeAPI.build_public()
for subscription in Subscription.objects.all(): for subscription in Subscription.objects.all():
__check_new_videos_sub(subscription, yt_api) __check_new_videos_sub(subscription, yt_api)
__detect_deleted(subscription) __detect_deleted(subscription)
@ -128,7 +124,7 @@ def synchronize_subscription(subscription: Subscription):
__lock.acquire() __lock.acquire()
try: try:
log.info("Running synchronization for single subscription %d [%s]", subscription.id, subscription.name) log.info("Running synchronization for single subscription %d [%s]", subscription.id, subscription.name)
yt_api = YoutubeAPI.build_public() yt_api = youtube.YoutubeAPI.build_public()
log.info("Sync - checking videos") log.info("Sync - checking videos")
__check_new_videos_sub(subscription, yt_api) __check_new_videos_sub(subscription, yt_api)
@ -148,12 +144,15 @@ def synchronize_subscription(subscription: Subscription):
def schedule_synchronize_global(): def schedule_synchronize_global():
trigger = CronTrigger.from_crontab(settings.get('global', 'SynchronizationSchedule')) trigger = CronTrigger.from_crontab(settings.get('global', 'SynchronizationSchedule'))
scheduler.instance.add_job(synchronize, trigger, max_instances=1, coalesce=True) job = scheduler.scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
log.info('Scheduled synchronize job job=%s', job.id)
def schedule_synchronize_now(): def schedule_synchronize_now():
scheduler.instance.add_job(synchronize, max_instances=1, coalesce=True) job = scheduler.scheduler.add_job(synchronize, max_instances=1, coalesce=True)
log.info('Scheduled synchronize now job job=%s', job.id)
def schedule_synchronize_now_subscription(subscription: Subscription): def schedule_synchronize_now_subscription(subscription: Subscription):
scheduler.instance.add_job(synchronize_subscription, args=[subscription]) job = scheduler.scheduler.add_job(synchronize_subscription, args=[subscription])
log.info('Scheduled synchronize subscription job subscription=(%s), job=%s', subscription, job.id)

View File

@ -1,114 +0,0 @@
from apscheduler.schedulers.background import BackgroundScheduler
from YtManagerApp.models import SubscriptionFolder, Subscription, Video, Channel
from YtManagerApp.utils.youtube import YoutubeAPI, YoutubeChannelInfo
class FolderManager(object):
@staticmethod
def create_or_edit(fid, name, parent_id):
# Create or edit
if fid == '#':
folder = SubscriptionFolder()
else:
folder = SubscriptionFolder.objects.get(id=int(fid))
# Set attributes
folder.name = name
if parent_id == '#':
folder.parent = None
else:
folder.parent = SubscriptionFolder.objects.get(id=int(parent_id))
FolderManager.__validate(folder)
folder.save()
@staticmethod
def __validate(folder: SubscriptionFolder):
# Make sure folder name is unique in the parent folder
for dbFolder in SubscriptionFolder.objects.filter(parent_id=folder.parent_id):
if dbFolder.id != folder.id and dbFolder.name == folder.name:
raise ValueError('Folder name is not unique!')
# Prevent parenting loops
current = folder
visited = []
while not (current is None):
if current in visited:
raise ValueError('Parenting cycle detected!')
visited.append(current)
current = current.parent
@staticmethod
def delete(fid: int):
folder = SubscriptionFolder.objects.get(id=fid)
folder.delete()
@staticmethod
def list_videos(fid: int):
folder = SubscriptionFolder.objects.get(id=fid)
folder_list = []
queue = [folder]
while len(queue) > 0:
folder = queue.pop()
folder_list.append(folder)
queue.extend(SubscriptionFolder.objects.filter(parent=folder))
return Video.objects.filter(subscription__parent_folder__in=folder_list).order_by('-publish_date')
class SubscriptionManager(object):
__scheduler = BackgroundScheduler()
@staticmethod
def create_or_edit(sid, url, name, parent_id):
# Create or edit
if sid == '#':
SubscriptionManager.create(url, parent_id, YoutubeAPI.build_public())
else:
sub = Subscription.objects.get(id=int(sid))
sub.name = name
if parent_id == '#':
sub.parent_folder = None
else:
sub.parent_folder = SubscriptionFolder.objects.get(id=int(parent_id))
sub.save()
@staticmethod
def create(url, parent_id, yt_api: YoutubeAPI):
sub = Subscription()
# Set parent
if parent_id == '#':
sub.parent_folder = None
else:
sub.parent_folder = SubscriptionFolder.objects.get(id=int(parent_id))
# Pull information about the channel and playlist
url_type, url_id = yt_api.parse_channel_url(url)
if url_type == 'playlist_id':
info_playlist = yt_api.get_playlist_info(url_id)
channel = SubscriptionManager.__get_or_create_channel('channel_id', info_playlist.getChannelId(), yt_api)
sub.name = info_playlist.getTitle()
sub.playlist_id = info_playlist.getId()
sub.description = info_playlist.getDescription()
sub.channel = channel
sub.icon_default = info_playlist.getDefaultThumbnailUrl()
sub.icon_best = info_playlist.getBestThumbnailUrl()
else:
channel = SubscriptionManager.__get_or_create_channel(url_type, url_id, yt_api)
# No point in getting the 'uploads' playlist info
sub.name = channel.name
sub.playlist_id = channel.upload_playlist_id
sub.description = channel.description
sub.channel = channel
sub.icon_default = channel.icon_default
sub.icon_best = channel.icon_best
sub.save()

View File

@ -1,25 +1,10 @@
from YtManagerApp.models import Subscription, Video, SubscriptionFolder
from YtManagerApp.utils.youtube import YoutubePlaylistItem
from typing import Optional
import re import re
from django.db.models import Q from typing import Optional
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q
from YtManagerApp.models import Subscription, Video, SubscriptionFolder
def create_video(yt_video: YoutubePlaylistItem, subscription: Subscription):
video = Video()
video.video_id = yt_video.getVideoId()
video.name = yt_video.getTitle()
video.description = yt_video.getDescription()
video.watched = False
video.downloaded_path = None
video.subscription = subscription
video.playlist_index = yt_video.getPlaylistIndex()
video.publish_date = yt_video.getPublishDate()
video.icon_default = yt_video.getDefaultThumbnailUrl()
video.icon_best = yt_video.getBestThumbnailUrl()
video.save()
return video
def get_videos(user: User, def get_videos(user: User,

View File

@ -0,0 +1,32 @@
# Generated by Django 2.1.2 on 2018-10-29 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('YtManagerApp', '0006_auto_20181027_0256'),
]
operations = [
migrations.RemoveField(
model_name='subscription',
name='channel',
),
migrations.AddField(
model_name='subscription',
name='channel_id',
field=models.CharField(default='test', max_length=128),
preserve_default=False,
),
migrations.AddField(
model_name='subscription',
name='channel_name',
field=models.CharField(default='Unknown', max_length=1024),
preserve_default=False,
),
migrations.DeleteModel(
name='Channel',
),
]

View File

@ -6,7 +6,7 @@ from django.contrib.auth.models import User
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.functions import Lower from django.db.models.functions import Lower
from YtManagerApp.utils.youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistInfo from YtManagerApp.utils import youtube
# help_text = user shown text # help_text = user shown text
# verbose_name = user shown name # verbose_name = user shown name
@ -176,6 +176,9 @@ class SubscriptionFolder(models.Model):
current = current.parent current = current.parent
return s[:-3] return s[:-3]
def __repr__(self):
return f'folder {self.id}, name="{self.name}"'
def delete_folder(self, keep_subscriptions: bool): def delete_folder(self, keep_subscriptions: bool):
if keep_subscriptions: if keep_subscriptions:
@ -225,92 +228,13 @@ class SubscriptionFolder(models.Model):
return data_collected return data_collected
class Channel(models.Model):
channel_id = models.TextField(null=False, unique=True)
username = models.TextField(null=True, unique=True)
custom_url = models.TextField(null=True, unique=True)
name = models.TextField()
description = models.TextField()
icon_default = models.TextField()
icon_best = models.TextField()
upload_playlist_id = models.TextField()
def __str__(self):
return self.name
@staticmethod
def find_by_channel_id(channel_id):
result = Channel.objects.filter(channel_id=channel_id)
if len(result) > 0:
return result.first()
return None
@staticmethod
def find_by_username(username):
result = Channel.objects.filter(username=username)
if len(result) > 0:
return result.first()
return None
@staticmethod
def find_by_custom_url(custom_url):
result = Channel.objects.filter(custom_url=custom_url)
if len(result) > 0:
return result.first()
return None
def fill(self, yt_channel_info: YoutubeChannelInfo):
self.channel_id = yt_channel_info.getId()
self.custom_url = yt_channel_info.getCustomUrl()
self.name = yt_channel_info.getTitle()
self.description = yt_channel_info.getDescription()
self.icon_default = yt_channel_info.getDefaultThumbnailUrl()
self.icon_best = yt_channel_info.getBestThumbnailUrl()
self.upload_playlist_id = yt_channel_info.getUploadsPlaylist()
self.save()
@staticmethod
def get_or_create(url_type: str, url_id: str, yt_api: YoutubeAPI):
channel: Channel = None
info_channel: YoutubeChannelInfo = None
if url_type == 'user':
channel = Channel.find_by_username(url_id)
if not channel:
info_channel = yt_api.get_channel_info_by_username(url_id)
channel = Channel.find_by_channel_id(info_channel.getId())
elif url_type == 'channel_id':
channel = Channel.find_by_channel_id(url_id)
if not channel:
info_channel = yt_api.get_channel_info(url_id)
elif url_type == 'channel_custom':
channel = Channel.find_by_custom_url(url_id)
if not channel:
found_channel_id = yt_api.search_channel(url_id)
channel = Channel.find_by_channel_id(found_channel_id)
if not channel:
info_channel = yt_api.get_channel_info(found_channel_id)
# If we downloaded information about the channel, store information
# about the channel here.
if info_channel:
if not channel:
channel = Channel()
if url_type == 'user':
channel.username = url_id
channel.fill(info_channel)
return channel
class Subscription(models.Model): class Subscription(models.Model):
name = models.CharField(null=False, max_length=1024) name = models.CharField(null=False, max_length=1024)
parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True) parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True)
playlist_id = models.CharField(null=False, max_length=128) playlist_id = models.CharField(null=False, max_length=128)
description = models.TextField() description = models.TextField()
channel = models.ForeignKey(Channel, on_delete=models.CASCADE) channel_id = models.CharField(max_length=128)
channel_name = models.CharField(max_length=1024)
icon_default = models.CharField(max_length=1024) icon_default = models.CharField(max_length=1024)
icon_best = models.CharField(max_length=1024) icon_best = models.CharField(max_length=1024)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
@ -327,30 +251,42 @@ class Subscription(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def fill_from_playlist(self, info_playlist: YoutubePlaylistInfo): def __repr__(self):
self.name = info_playlist.getTitle() return f'subscription {self.id}, name="{self.name}", playlist_id="{self.playlist_id}"'
self.playlist_id = info_playlist.getId()
self.description = info_playlist.getDescription()
self.icon_default = info_playlist.getDefaultThumbnailUrl()
self.icon_best = info_playlist.getBestThumbnailUrl()
def copy_from_channel(self): def fill_from_playlist(self, info_playlist: youtube.Playlist):
self.name = info_playlist.title
self.playlist_id = info_playlist.id
self.description = info_playlist.description
self.channel_id = info_playlist.channel_id
self.channel_name = info_playlist.channel_title
self.icon_default = youtube.default_thumbnail(info_playlist).url
self.icon_best = youtube.best_thumbnail(info_playlist).url
def copy_from_channel(self, info_channel: youtube.Channel):
# No point in storing info about the 'uploads from X' playlist # No point in storing info about the 'uploads from X' playlist
self.name = self.channel.name self.name = info_channel.title
self.playlist_id = self.channel.upload_playlist_id self.playlist_id = info_channel.uploads_playlist.id
self.description = self.channel.description self.description = info_channel.description
self.icon_default = self.channel.icon_default self.channel_id = info_channel.id
self.icon_best = self.channel.icon_best self.channel_name = info_channel.title
self.icon_default = youtube.default_thumbnail(info_channel).url
self.icon_best = youtube.best_thumbnail(info_channel).url
def fetch_from_url(self, url, yt_api: youtube.YoutubeAPI):
url_parsed = yt_api.parse_url(url)
if 'playlist' in url_parsed:
info_playlist = yt_api.playlist(url=url)
if info_playlist is None:
raise ValueError('Invalid playlist ID!')
def fetch_from_url(self, url, yt_api: YoutubeAPI):
url_type, url_id = yt_api.parse_channel_url(url)
if url_type == 'playlist_id':
info_playlist = yt_api.get_playlist_info(url_id)
self.channel = Channel.get_or_create('channel_id', info_playlist.getChannelId(), yt_api)
self.fill_from_playlist(info_playlist) self.fill_from_playlist(info_playlist)
else: else:
self.channel = Channel.get_or_create(url_type, url_id, yt_api) info_channel = yt_api.channel(url=url)
self.copy_from_channel() if info_channel is None:
raise ValueError('Cannot find channel!')
self.copy_from_channel(info_channel)
def delete_subscription(self, keep_downloaded_videos: bool): def delete_subscription(self, keep_downloaded_videos: bool):
self.delete() self.delete()
@ -383,6 +319,22 @@ class Video(models.Model):
views = models.IntegerField(null=False, default=0) views = models.IntegerField(null=False, default=0)
rating = models.FloatField(null=False, default=0.5) rating = models.FloatField(null=False, default=0.5)
@staticmethod
def create(playlist_item: youtube.PlaylistItem, subscription: Subscription):
video = Video()
video.video_id = playlist_item.resource_video_id
video.name = playlist_item.title
video.description = playlist_item.description
video.watched = False
video.downloaded_path = None
video.subscription = subscription
video.playlist_index = playlist_item.position
video.publish_date = playlist_item.published_at
video.icon_default = youtube.default_thumbnail(playlist_item).url
video.icon_best = youtube.best_thumbnail(playlist_item).url
video.save()
return video
def mark_watched(self): def mark_watched(self):
self.watched = True self.watched = True
self.save() self.save()
@ -428,3 +380,6 @@ class Video(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def __repr__(self):
return f'video {self.id}, video_id="{self.video_id}"'

View File

@ -2,12 +2,12 @@ import logging
import sys import sys
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
instance: BackgroundScheduler = None scheduler: BackgroundScheduler = None
def initialize_scheduler(): def initialize_scheduler():
from .appconfig import settings from .appconfig import settings
global instance global scheduler
logger = logging.getLogger('scheduler') logger = logging.getLogger('scheduler')
executors = { executors = {
@ -17,8 +17,8 @@ def initialize_scheduler():
} }
} }
job_defaults = { job_defaults = {
'misfire_grace_time': sys.maxsize 'misfire_grace_time': 60 * 60 * 24 * 365 # 1 year
} }
instance = BackgroundScheduler(logger=logger, executors=executors, job_defaults=job_defaults) scheduler = BackgroundScheduler(logger=logger, executors=executors, job_defaults=job_defaults)
instance.start() scheduler.start()

View File

@ -1,32 +0,0 @@
import itertools
from typing import Iterable
def first_true(*args, default=False, pred=None):
"""Returns the first true value in the iterable.
If no true value is found, returns *default*
If *pred* is not None, returns the first item
for which pred(item) is true.
"""
# first_true([a,b,c], x) --> a or b or c or x
# first_true([a,b], x, f) --> a if f(a) else b if f(b) else x
return next(filter(pred, args), default)
def as_chunks(iterable: Iterable, chunk_size: int):
"""
Iterates an iterable in chunks of chunk_size elements.
:param iterable: An iterable containing items to iterate.
:param chunk_size: Chunk size
:return: Returns a generator which will yield chunks of size chunk_size
"""
it = iter(iterable)
while True:
chunk = tuple(itertools.islice(it, chunk_size))
if not chunk:
return
yield chunk

View File

@ -1,285 +1,48 @@
from googleapiclient.discovery import build
from googleapiclient.errors import Error as APIError
from google_auth_oauthlib.flow import InstalledAppFlow
from django.conf import settings from django.conf import settings
import re from external.pytaw.pytaw.youtube import YouTube, Channel, Playlist, PlaylistItem, Thumbnail, InvalidURL, Resource, Video
from YtManagerApp.utils.iterutils import as_chunks from typing import Optional
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
YOUTUBE_LIST_LIMIT = 50
class YoutubeException(Exception): class YoutubeAPI(YouTube):
pass
class YoutubeInvalidURLException(YoutubeException):
pass
class YoutubeChannelNotFoundException(YoutubeException):
pass
class YoutubeUserNotFoundException(YoutubeException):
pass
class YoutubePlaylistNotFoundException(YoutubeException):
pass
class YoutubeVideoNotFoundException(YoutubeException):
pass
class YoutubeChannelInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
self.__snippet = result_dict['snippet']
self.__contentDetails = result_dict['contentDetails']
def getId(self):
return self.__id
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getCustomUrl(self):
try:
return self.__snippet['customUrl']
except KeyError:
return None
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
def getUploadsPlaylist(self):
return self.__contentDetails['relatedPlaylists']['uploads']
class YoutubePlaylistInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
self.__snippet = result_dict['snippet']
def getId(self):
return self.__id
def getChannelId(self):
return self.__snippet['channelId']
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
class YoutubePlaylistItem(object):
def __init__(self, result_dict):
self.__snippet = result_dict['snippet']
def getVideoId(self):
return self.__snippet['resourceId']['videoId']
def getPublishDate(self):
return self.__snippet['publishedAt']
def getTitle(self):
return self.__snippet['title']
def getDescription(self):
return self.__snippet['description']
def getDefaultThumbnailUrl(self):
return self.__snippet['thumbnails']['default']['url']
def getBestThumbnailUrl(self):
best_url = None
best_res = 0
for _, thumb in self.__snippet['thumbnails'].items():
res = thumb['width'] * thumb['height']
if res > best_res:
best_res = res
best_url = thumb['url']
return best_url
def getPlaylistIndex(self):
return self.__snippet['position']
class YoutubeVideoStatistics(object):
def __init__(self, result_dict):
self.id = result_dict['id']
self.stats = result_dict['statistics']
def get_view_count(self):
return int(self.stats['viewCount'])
def get_like_count(self):
return int(self.stats['likeCount'])
def get_dislike_count(self):
return int(self.stats['dislikeCount'])
def get_favorite_count(self):
return int(self.stats['favoriteCount'])
def get_comment_count(self):
return int(self.stats['commentCount'])
class YoutubeAPI(object):
def __init__(self, service):
self.service = service
@staticmethod @staticmethod
def build_public() -> 'YoutubeAPI': def build_public() -> 'YoutubeAPI':
service = build(API_SERVICE_NAME, API_VERSION, developerKey=settings.YOUTUBE_API_KEY) return YoutubeAPI(key=settings.YOUTUBE_API_KEY)
return YoutubeAPI(service)
@staticmethod
def parse_channel_url(url):
"""
Parses given channel url, returns a tuple of the form (type, value), where type can be one of:
* channel_id
* channel_custom
* user
* playlist_id
:param url: URL to parse
:return: (type, value) tuple
"""
match = re.search(r'youtube\.com/.*[&?]list=([^?&/]+)', url)
if match:
return 'playlist_id', match.group(1)
match = re.search(r'youtube\.com/user/([^?&/]+)', url)
if match:
return 'user', match.group(1)
match = re.search(r'youtube\.com/channel/([^?&/]+)', url)
if match:
return 'channel_id', match.group(1)
match = re.search(r'youtube\.com/(?:c/)?([^?&/]+)', url)
if match:
return 'channel_custom', match.group(1)
raise YoutubeInvalidURLException('Unrecognized URL format!')
def get_playlist_info(self, list_id) -> YoutubePlaylistInfo:
result = self.service.playlists()\
.list(part='snippet', id=list_id)\
.execute()
if len(result['items']) <= 0:
raise YoutubePlaylistNotFoundException("Invalid playlist ID.")
return YoutubePlaylistInfo(result['items'][0])
def get_channel_info_by_username(self, user) -> YoutubeChannelInfo:
result = self.service.channels()\
.list(part='snippet,contentDetails', forUsername=user)\
.execute()
if len(result['items']) <= 0:
raise YoutubeUserNotFoundException('Invalid user.')
return YoutubeChannelInfo(result['items'][0])
def get_channel_info(self, channel_id) -> YoutubeChannelInfo:
result = self.service.channels()\
.list(part='snippet,contentDetails', id=channel_id)\
.execute()
if len(result['items']) <= 0:
raise YoutubeChannelNotFoundException('Invalid channel ID.')
return YoutubeChannelInfo(result['items'][0])
def search_channel(self, custom) -> str:
result = self.service.search()\
.list(part='id', q=custom, type='channel')\
.execute()
if len(result['items']) <= 0:
raise YoutubeChannelNotFoundException('Could not find channel!')
channel_result = result['items'][0]
return channel_result['id']['channelId']
def list_playlist_videos(self, playlist_id):
kwargs = {
"part": "snippet",
"maxResults": 50,
"playlistId": playlist_id
}
last_page = False
while not last_page:
result = self.service.playlistItems()\
.list(**kwargs)\
.execute()
for item in result['items']:
yield YoutubePlaylistItem(item)
if 'nextPageToken' in result:
kwargs['pageToken'] = result['nextPageToken']
else:
last_page = True
def get_single_video_stats(self, video_id) -> YoutubeVideoStatistics:
result = list(self.get_video_stats([video_id]))
if len(result) < 1:
raise YoutubeVideoNotFoundException('Could not find video with id ' + video_id + '!')
return result[0]
def get_video_stats(self, video_id_list):
for chunk in as_chunks(video_id_list, YOUTUBE_LIST_LIMIT):
kwargs = {
"part": "statistics",
"maxResults": YOUTUBE_LIST_LIMIT,
"id": ','.join(chunk)
}
result = self.service.videos()\
.list(**kwargs)\
.execute()
for item in result['items']:
yield YoutubeVideoStatistics(item)
# @staticmethod # @staticmethod
# def build_oauth() -> 'YoutubeAPI': # def build_oauth() -> 'YoutubeAPI':
# flow = # flow =
# credentials = # credentials =
# service = build(API_SERVICE_NAME, API_VERSION, credentials) # service = build(API_SERVICE_NAME, API_VERSION, credentials)
def default_thumbnail(resource: Resource) -> Optional[Thumbnail]:
"""
Gets the default thumbnail for a resource.
Searches in the list of thumbnails for one with the label 'default', or takes the first one.
:param resource:
:return:
"""
thumbs = getattr(resource, 'thumbnails', None)
if thumbs is None or len(thumbs) <= 0:
return None
return next(
(i for i in thumbs if i.id == 'default'),
thumbs[0]
)
def best_thumbnail(resource: Resource) -> Optional[Thumbnail]:
"""
Gets the best thumbnail available for a resource.
:param resource:
:return:
"""
thumbs = getattr(resource, 'thumbnails', None)
if thumbs is None or len(thumbs) <= 0:
return None
return max(thumbs, key=lambda t: t.width * t.height)

View File

@ -1,4 +1,4 @@
from crispy_forms.helper import FormHelperpython3 from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, HTML from crispy_forms.layout import Layout, Field, HTML
from django import forms from django import forms
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -175,7 +175,8 @@ class SubscriptionFolderForm(forms.ModelForm):
args_id.append(~Q(id=self.instance.id)) args_id.append(~Q(id=self.instance.id))
if SubscriptionFolder.objects.filter(parent=parent, name__iexact=name, *args_id).count() > 0: if SubscriptionFolder.objects.filter(parent=parent, name__iexact=name, *args_id).count() > 0:
raise forms.ValidationError('A folder with the same name already exists in the given parent directory!', code='already_exists') raise forms.ValidationError(
'A folder with the same name already exists in the given parent directory!', code='already_exists')
# Check for cycles # Check for cycles
if self.instance is not None: if self.instance is not None:
@ -238,6 +239,7 @@ class CreateSubscriptionForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.yt_api = youtube.YoutubeAPI.build_public()
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False
self.helper.layout = Layout( self.helper.layout = Layout(
@ -252,11 +254,18 @@ class CreateSubscriptionForm(forms.ModelForm):
) )
def clean_playlist_url(self): def clean_playlist_url(self):
playlist_url = self.cleaned_data['playlist_url'] playlist_url: str = self.cleaned_data['playlist_url']
try: try:
youtube.YoutubeAPI.parse_channel_url(playlist_url) parsed_url = self.yt_api.parse_url(playlist_url)
except youtube.YoutubeInvalidURLException: except youtube.InvalidURL as e:
raise forms.ValidationError('Invalid playlist/channel URL, or not in a recognized format.') raise forms.ValidationError(str(e))
is_playlist = 'playlist' in parsed_url
is_channel = parsed_url['type'] in ('channel', 'user', 'channel_custom')
if not is_channel and not is_playlist:
raise forms.ValidationError('The given URL must link to a channel or a playlist!')
return playlist_url return playlist_url
@ -269,21 +278,22 @@ class CreateSubscriptionModal(LoginRequiredMixin, ModalMixin, CreateView):
api = youtube.YoutubeAPI.build_public() api = youtube.YoutubeAPI.build_public()
try: try:
form.instance.fetch_from_url(form.cleaned_data['playlist_url'], api) form.instance.fetch_from_url(form.cleaned_data['playlist_url'], api)
except youtube.YoutubeChannelNotFoundException: except youtube.InvalidURL as e:
return self.modal_response( return self.modal_response(form, False, str(e))
form, False, 'Could not find a channel based on the given URL. Please verify that the URL is correct.') except ValueError as e:
except youtube.YoutubeUserNotFoundException: return self.modal_response(form, False, str(e))
return self.modal_response( # except youtube.YoutubeUserNotFoundException:
form, False, 'Could not find an user based on the given URL. Please verify that the URL is correct.') # return self.modal_response(
except youtube.YoutubePlaylistNotFoundException: # form, False, 'Could not find an user based on the given URL. Please verify that the URL is correct.')
return self.modal_response( # except youtube.YoutubePlaylistNotFoundException:
form, False, 'Could not find a playlist based on the given URL. Please verify that the URL is correct.') # return self.modal_response(
except youtube.YoutubeException as e: # form, False, 'Could not find a playlist based on the given URL. Please verify that the URL is correct.')
return self.modal_response( # except youtube.YoutubeException as e:
form, False, str(e)) # return self.modal_response(
except youtube.APIError as e: # form, False, str(e))
return self.modal_response( # except youtube.APIError as e:
form, False, 'An error occurred while communicating with the YouTube API: ' + str(e)) # return self.modal_response(
# form, False, 'An error occurred while communicating with the YouTube API: ' + str(e))
return super().form_valid(form) return super().form_valid(form)

View File

@ -41,7 +41,7 @@ LogLevel=DEBUG
;DownloadOrder=playlist ;DownloadOrder=playlist
; Path where downloaded videos are stored ; Path where downloaded videos are stored
DownloadPath=D:\\Dev\\youtube-channel-manager\\temp\\download DownloadPath=temp/download
; A pattern which describes how downloaded files are organized. Extensions are automatically appended. ; A pattern which describes how downloaded files are organized. Extensions are automatically appended.
; Supported fields: channel, channel_id, playlist, playlist_id, playlist_index, title, id ; Supported fields: channel, channel_id, playlist, playlist_id, playlist_index, title, id

0
external/__init__.py vendored Normal file
View File

19
external/pytaw/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
*.bak
*.egg
*.egg-info/
*.eggs/
*.pyproj
*.sln
*.vs/
*~
.DS_Store
.cache/
.coverage
.idea/
.tox/
_build/
build/
dist/
__pycache__/
*.ini

3
external/pytaw/.pytaw.conf vendored Normal file
View File

@ -0,0 +1,3 @@
; by default pytaw will look for this file (".pytaw.conf") in the user's home directory
[youtube]
developer_key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

29
external/pytaw/README.md vendored Normal file
View File

@ -0,0 +1,29 @@
# PYTAW: Python YouTube API Wrapper
###Note
This library is copied from [https://github.com/chibicitiberiu/pytaw/tree/improvements](https://github.com/chibicitiberiu/pytaw/tree/improvements).
```python
>>> from pytaw import YouTube
>>> youtube = YouTube(key='your_api_key')
>>> video = youtube.video('4vuW6tQ0218')
>>> video.title
'Monty Python - Dead Parrot'
>>> video.published_at
datetime.datetime(2007, 2, 14, 13, 55, 51, tzinfo=tzutc())
>>> channel = video.channel
>>> channel.title
'Chadner'
>>> search = youtube.search(q='monty python')
>>> search[0]
<Channel UCGm3CO6LPcN-Y7HIuyE0Rew "Monty Python">
>>> for r in search[:5]:
... print(r)
...
Monty Python
Chemist Sketch - Monty Python's Flying Circus
A Selection of Sketches from "Monty Python's Flying Circus" - #4
Monty Python - Dead Parrot
Monty Python And the holy grail
```

0
external/pytaw/__init__.py vendored Normal file
View File

20
external/pytaw/docs/Makefile vendored Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = pytaw
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

169
external/pytaw/docs/conf.py vendored Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# pytaw documentation build configuration file, created by
# sphinx-quickstart on Mon Nov 27 19:26:35 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'pytaw'
copyright = '2017, 6000hulls'
author = '6000hulls'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'pytawdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pytaw.tex', 'pytaw Documentation',
'6000hulls', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pytaw', 'pytaw Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pytaw', 'pytaw Documentation',
author, 'pytaw', 'One line description of project.',
'Miscellaneous'),
]

18
external/pytaw/docs/index.rst vendored Normal file
View File

@ -0,0 +1,18 @@
PYTAW: Python YouTube API Wrapper
=================================
It's a wrapper for the YouTube python API. Written in python.
.. automodule:: pytaw.youtube
:members:
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

36
external/pytaw/docs/make.bat vendored Normal file
View File

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=pytaw
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

13
external/pytaw/main_test.py vendored Normal file
View File

@ -0,0 +1,13 @@
import pytaw
yt = pytaw.YouTube(key='AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8')
c = yt.channel('UCmmPgObSUPw1HL2lq6H4ffA')
uploads_playlist = c.uploads_playlist
print(repr(uploads_playlist))
uploads_list = list(uploads_playlist.items)
for item in uploads_list:
print(item.position, '...', repr(item), ' .... ', repr(item.video))
print(item.thumbnails)
break

1
external/pytaw/pytaw/__init__.py vendored Normal file
View File

@ -0,0 +1 @@
from .youtube import YouTube

92
external/pytaw/pytaw/utils.py vendored Normal file
View File

@ -0,0 +1,92 @@
import re
import urllib.parse
import typing
from datetime import datetime, timezone
import dateutil.parser
import itertools
def string_to_datetime(string):
if string is None:
return None
else:
return dateutil.parser.parse(string)
def datetime_to_string(dt):
if dt is None:
return None
if dt.tzinfo is None:
dt = dt.astimezone(timezone.utc)
return dt.isoformat()
def youtube_url_to_id(url):
"""Extract video id from a youtube url.
If parsing fails, try regex. If that fails, return None.
The regex is from somewhere in this thread, I think:
https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url
"""
url = urllib.parse.unquote(url)
url_data = urllib.parse.urlparse(url)
query = urllib.parse.parse_qs(url_data.query)
try:
# parse the url for a video query
return query["v"][0]
except KeyError:
# use regex to try and extract id
match = re.search(
r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)",
url,
)
if match:
return match.group()
else:
return None
def youtube_duration_to_seconds(value):
"""Convert youtube (ISO 8601) duration to seconds.
https://en.wikipedia.org/wiki/ISO_8601#Durations
https://regex101.com/r/ALmmSS/1
"""
iso8601 = r"P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?"
match = re.match(iso8601, value)
if match is None:
return None
group_names = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']
d = dict()
for name, group in zip(group_names, match.groups(default=0)):
d[name] = int(group)
return int(
d['years']*365*24*60*60 +
d['months']*30*24*60*60 +
d['weeks']*7*24*60*60 +
d['days']*24*60*60 +
d['hours']*60*60 +
d['minutes']*60 +
d['seconds']
)
def iterate_chunks(iterable: typing.Iterable, chunk_size: int):
"""
Iterates an iterable in chunks of chunk_size elements.
:param iterable: An iterable containing items to iterate.
:param chunk_size: Chunk size
:return: Returns a generator which will yield chunks of size chunk_size
"""
it = iter(iterable)
while True:
chunk = tuple(itertools.islice(it, chunk_size))
if not chunk:
return
yield chunk

1055
external/pytaw/pytaw/youtube.py vendored Normal file

File diff suppressed because it is too large Load Diff

12
external/pytaw/setup.py vendored Normal file
View File

@ -0,0 +1,12 @@
from setuptools import setup
setup(
name='pytaw',
version='0.0.1',
packages=['pytaw'],
url='https://github.com/6000hulls/pytaw',
license='',
author='6000hulls',
author_email='6000hulls@gmail.com',
description='PYTAW: Python YouTube API Wrapper'
)

0
external/pytaw/tests/__init__.py vendored Normal file
View File

165
external/pytaw/tests/test_pytaw.py vendored Normal file
View File

@ -0,0 +1,165 @@
import pytest
import logging
import sys
import collections
from datetime import datetime, timedelta
from googleapiclient.errors import HttpError
from pytaw import YouTube
from pytaw.youtube import Resource, Video, AttributeDef
logging.basicConfig(stream=sys.stdout) # show log output when run with pytest -s
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
@pytest.fixture
def youtube():
"""A YouTube instance initialised with a developer key loaded from config.ini"""
return YouTube()
@pytest.fixture
def video(youtube):
"""A Video instance for the classic video 'Me at the zoo'"""
return youtube.video('jNQXAC9IVRw')
@pytest.fixture
def channel(youtube):
"""A Channel instance for the 'YouTube Help' channel"""
return youtube.channel('UCMDQxm7cUx3yXkfeHa5zJIQ')
@pytest.fixture
def search(youtube):
"""A ListResponse instance corresponding to a search for the query 'python'"""
return youtube.search()
@pytest.fixture
def video_search(youtube):
"""A ListResponse instance corresponding to a video search for the query 'python'"""
return youtube.search(q='python', type='video')
@pytest.fixture
def video_search_array(youtube):
"""An array of video searches with a wide range of results (zero to millions)."""
one_minute_ago = datetime.utcnow() - timedelta(minutes=1)
five_minutes_ago = datetime.utcnow() - timedelta(minutes=5)
return [
#
# no results
youtube.search(q='minecraft', type='video', publishedBefore=datetime(2000, 1, 1)),
#
# less than 100 results
youtube.search(q='minecraft', type='video', publishedBefore=datetime(2005, 7, 1)),
#
# over 100 results
youtube.search(q='minecraft', type='video', publishedBefore=datetime(2006, 1, 1)),
#
# variable number of results (hundreds or thousands...?)
youtube.search(q='minecraft', type='video', publishedAfter=one_minute_ago),
youtube.search(q='minecraft', type='video', publishedAfter=five_minutes_ago),
#
# over a million results
youtube.search(q='minecraft', type='video'),
youtube.search(q='minecraft'),
]
class TestResource:
def test_equality(self, search):
a = search[0]
b = search[0]
c = search[1]
assert a == b
assert a != c
def test_unknown_attribute(self, video):
with pytest.raises(AttributeError):
_ = video.attribute_name_which_definitely_will_never_exist
def test_unknown_part_in_attributedef(self, video):
video.ATTRIBUTE_DEFS['x'] = AttributeDef('nonexistant_part', 'x')
with pytest.raises(HttpError):
_ = video.x
def test_unknown_attribute_name_in_attributedef(self, video):
video.ATTRIBUTE_DEFS['x'] = AttributeDef('snippet', 'nonexistant_attribute')
assert video.x is None
class TestVideo:
def test_bad_video_id(self, youtube):
video = youtube.video('not_a_valid_youtube_video_id')
assert video is None
def test_title(self, video):
assert video.title == "Me at the zoo"
def test_published_at(self, video):
assert video.published_at.isoformat() == '2005-04-24T03:31:52+00:00'
def test_n_views(self, video):
assert video.n_views > int(40e6)
def test_tags(self, video):
assert video.tags == ['jawed', 'karim', 'elephant', 'zoo', 'youtube', 'first', 'video']
def test_duration(self, video):
assert video.duration.total_seconds() == 19
class TestChannel:
def test_title(self, channel):
assert channel.title == "YouTube Help"
class TestSearch:
def test_video_search_returns_a_video(self, video_search):
assert isinstance(video_search[0], Video)
def test_video_search_has_many_results(self, video_search):
# make video_search unlazy (populate pageInfo attributes)
_ = video_search[0]
assert video_search.total_results > 10000
def test_search_iteration(self, search):
"""Simply iterate over a search, creating all resources, to check for exceptions."""
for resource in search:
log.debug(resource)
class TestListResponse:
def test_if_iterable(self, search):
assert isinstance(search, collections.Iterator)
def test_integer_indexing(self, search):
assert isinstance(search[0], Resource)
def test_slice_indexing(self, search):
assert isinstance(search[1:3], list)
def test_full_listing_iteration(self, video_search_array):
"""Iterate over all search results to check no exceptions are raised when paging etc.
Even if millions of results are found, the API will never return more than 500 (by
design), so we're okay to just bang right through the search results generator for the
whole array of video searches.
"""
for i, search in enumerate(video_search_array):
c = 0
for _ in search:
c += 1
log.debug(f"checked first {c} results (search #{i})")

View File

@ -11,3 +11,4 @@ A self-hosted tool which manages your YouTube subscriptions, and downloads files
* google-api-python-client: `$ pip3 install google-api-python-client` * google-api-python-client: `$ pip3 install google-api-python-client`
* google_auth_oauthlib: `$ pip3 install google_auth_oauthlib` * google_auth_oauthlib: `$ pip3 install google_auth_oauthlib`
* apscheduler (v3.5+): `$ pip3 install apscheduler` * apscheduler (v3.5+): `$ pip3 install apscheduler`
* (recommended) oauth2client: `$ pip3 install oauth2client`