Merge pull request #22 from chibicitiberiu/development

Load all settings from config.ini, improve docker support and cleanup
This commit is contained in:
chibicitiberiu 2018-11-02 22:08:29 +02:00 committed by GitHub
commit d989cf4132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 340 additions and 2405 deletions

8
.gitignore vendored
View File

@ -115,10 +115,16 @@ venv.bak/
.dmypy.json
dmypy.json
media/
data/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea
# Dolphin generated file
.directory

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal">
<data-source name="Django default" uuid="2dac2136-d902-4d27-8789-9371934602fd">
<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" />
<auth-required>false</auth-required>
<introspection-schemas>*:@</introspection-schemas>
</data-source>
</component>
</project>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="Django default" uuid="2dac2136-d902-4d27-8789-9371934602fd">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/YtManager/settings.py</remarks>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:D:\Dev\youtube-channel-manager\db.sqlite3</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</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>
</project>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="Django default">
<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="sqlite_master">
<System>1</System>
</table>
<column id="7" parent="6" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="8" parent="6" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="9" parent="6" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="10" parent="6" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
</column>
<column id="11" parent="6" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
</database-model>
</dataSource>

View File

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

View File

@ -1,883 +0,0 @@
<?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

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

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{bootstrap, jquery-3.3.1, jstree, popper}" />
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6" project-jdk-type="Python SDK" />
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/youtube-channel-manager.iml" filepath="$PROJECT_DIR$/.idea/youtube-channel-manager.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="$FileName$ $FileNameWithoutExtension$.css --source-map" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="less" />
<option name="immediateSync" value="true" />
<option name="name" value="Less" />
<option name="output" value="$FileNameWithoutExtension$.css:$FileNameWithoutExtension$.css.map" />
<option name="outputFilters">
<array>
<FilterInfo>
<option name="description" value="" />
<option name="name" value="" />
<option name="regExp" value="$MESSAGE$\Q in \E$FILE_PATH$\Q on line \E$LINE$\Q, column \E$COLUMN$" />
</FilterInfo>
</array>
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$USER_HOME$/AppData/Roaming/npm/lessc.cmd" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$FileDir$" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="true">
<option name="arguments" value="--no-cache --update $FileName$:$FileNameWithoutExtension$.css" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="scss" />
<option name="immediateSync" value="true" />
<option name="name" value="SCSS" />
<option name="output" value="$FileNameWithoutExtension$.css:$FileNameWithoutExtension$.css.map" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="scss" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$FileDir$" />
<envs />
</TaskOptions>
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="YtManager/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.6" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jquery-3.3.1" level="application" />
<orderEntry type="library" name="popper" level="application" />
<orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="popper" level="application" />
<orderEntry type="library" name="jquery-3.3.1" level="application" />
<orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="jstree" level="application" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/YtManagerApp/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

View File

@ -1,27 +1,23 @@
FROM python:3
WORKDIR /usr/src/app
WORKDIR /usr/src/ytsm/app
# ffmpeg is needed for youtube-dl
RUN apt-get update
RUN apt-get install ffmpeg -y
COPY ./app/requirements.txt ./
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
ENV YTSM_DATABASE_ENGINE='django.db.backends.sqlite3'
ENV YTSM_DATABASE_NAME='/usr/src/app/data/db/ytmanager.db'
ENV YTSM_DATABASE_HOST=''
ENV YTSM_DATABASE_USERNAME=''
ENV YTSM_DATABASE_PASSWORD=''
ENV YTSM_DATABASE_PORT=''
ENV YTSM_YOUTUBE_API_KEY='AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
ENV YTSM_DEBUG='False'
ENV YTSM_DATA_PATH='/usr/src/ytsm/data'
VOLUME /usr/src/app/data/media
VOLUME /usr/src/app/data/db
VOLUME /usr/src/ytsm/config
VOLUME /usr/src/ytsm/data
COPY ./app/ .
COPY ./config/ ./config/
COPY ./app/ ./
COPY ./docker/init.sh ./
EXPOSE 8000
CMD ["/bin/bash", "init.sh"]
CMD ["/bin/bash", "init.sh"]

105
README.md
View File

@ -24,8 +24,10 @@ Of course, there are a lot of things that still need to be done. The web interfa
* python3: `$ apt install python3`
* pip: `$ apt install python3-pip`
* ffmpeg: `$ apt install ffmpeg`
* django: `$ pip3 install django`
* crispy_forms: `$ pip3 install django-crispy-forms`
* dj-config-url: `$ pip3 install dj-config-url`
* youtube-dl: `$ pip3 install youtube-dl`
* google-api-python-client: `$ pip3 install google-api-python-client`
* google_auth_oauthlib: `$ pip3 install google_auth_oauthlib`
@ -34,65 +36,104 @@ Of course, there are a lot of things that still need to be done. The web interfa
## Installation
There are 2 ways you can install this server. Using docker is the quickest and easiest method.
### Normal installation for development/testing
1. Install all the dependencies listed above.
```bash
sudo apt install python3 python3-pip
sudo pip3 install apscheduler django django-crispy-forms youtube-dl google-api-python-client google_auth_oauthlib oauth2client
```
2. Clone this repository:
1. Clone this repository:
```bash
git clone https://github.com/chibicitiberiu/ytsm.git
cd ytsm
```
3. Set up the database: `python3 manage.py migrate`
By default, a SQLite database is used, which is located in the project's folder.
You can customize that in `YtManager/settings.py`, by modifying the `DATABASES` variable (search Django documentation for details).
4. Set up the `MEDIA_ROOT` variable in `YtManager/settings.py`. This is where the thumbnails will be downloaded.
(note: this will be moved to `config.ini` in the future).
2. Install all the dependencies listed above.
5. Obtain an YouTube API developer key from [https://console.developers.google.com/apis/dashboard](https://console.developers.google.com/apis/dashboard).
```bash
sudo apt install python3 python3-pip ffmpeg
sudo pip3 install --no-cache-dir -r requirements.txt
```
3. Modify `config/config.ini` to your liking. All the settings should be documented through comments.
All these settings apply server-wide. The settings in the `user` section can be overriden from the web page for each
individual user.
4. Obtain an YouTube API developer key from [https://console.developers.google.com/apis/dashboard](https://console.developers.google.com/apis/dashboard).
You can find a detailed guide on [this page](https://www.slickremix.com/docs/get-api-key-for-youtube/).
The `defaults.ini` file already has an API key, but if the quotas are reached, you won't be able to use this program
any more. Also, I might decide to delete that key, which will break your installation.
After obtaining the key, set it in `config.ini`.
6. Modify `config/config.ini` to your liking. All the settings should be documented through comments.
All these settings apply server-wide. The settings in the `user` section can be overriden from the web page for each
individual user.
5. Set up the database:
The most important settings are:
* `[Global] YoutubeApiKey` - put your YouTube API key here
* `[User] DownloadPath` - sets the folder where videos will be downloaded
7. Start the server: `python3 manage.py runserver [port] --noreload`
```bash
cd app
python3 manage.py migrate
```
By default, a SQLite database is used, which is located in the project's folder. The database can be configured
in `settings.ini`.
6. Start the server: `python3 manage.py runserver [port] --noreload --insecure`
The `port` parameter is optional.
The `--noreload` option is necessary, otherwise the scheduler will run on 2 separate processes at the same time,
which is not ideal.
which is not ideal.
The `--insecure` option is required only if `Debug=False` in `config.ini`, Without this option, the static resources
(CSS, javascript) won't work.
8. Open the server's page in your browser, by entering `http://localhost:port` in your address bar.
7. Open the server's page in your browser, by entering `http://localhost:port` in your address bar.
9. Create an admin user by going to the *register* page, and creating an user account.
8. Create an admin user by going to the *register* page, and creating an user account.
10. Add some subscriptions, and enjoy!
9. Add some subscriptions, and enjoy!
### Docker
A much easier way to install is to use Docker.
1. Clone this repository:
To run with docker, edit the config file (config/config.ini) and then run `docker-compose up -d`, it will bind to port 80.
```bash
git clone https://github.com/chibicitiberiu/ytsm.git
cd ytsm
```
You can edit the default download locations in the docker-compose.yml file.
2. Install docker (if not installed)
3. Modify `config/config.ini` to your liking. All the settings should be documented through comments.
All these settings apply server-wide. The settings in the `user` section can be overriden from the web page for each
individual user.
**Attention**: you cannot modify the download location from `settings.ini` when using docker.
To do so, you will need to modify the volume mapping in `docker-compose.yml`.
4. Obtain an YouTube API developer key from [https://console.developers.google.com/apis/dashboard](https://console.developers.google.com/apis/dashboard).
You can find a detailed guide on [this page](https://www.slickremix.com/docs/get-api-key-for-youtube/).
The `defaults.ini` file already has an API key, but if the quotas are reached, you won't be able to use this program
any more. Also, I might decide to delete that key, which will break your installation.
After obtaining the key, set it in `config.ini`.
5. Build and run docker compose image:
```bash
docker-compose up -d
```
6. Open the server's page in your browser, by entering `http://localhost` in your address bar.
7. Create an admin user by going to the *register* page, and creating an user account.
8. Add some subscriptions, and enjoy!
The docker image uses a sqlite database, and stores the data in a folder `data/` located in the project directory.
You can edit the default download locations in the `docker-compose.yml` file.
For more information about using Docker, check [this page](Docker_README.md).
### Deploying for production

View File

@ -11,23 +11,13 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
import logging
from os.path import dirname as up
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0'
YOUTUBE_API_KEY = os.getenv('YTSM_YOUTUBE_API_KEY', 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
#
# Basic Django stuff
#
ALLOWED_HOSTS = ['*']
SESSION_COOKIE_AGE = 3600 * 30 # one month
# Application definition
@ -76,24 +66,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'YtManager.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': os.getenv('YTSM_DATABASE_ENGINE', 'django.db.backends.sqlite3'),
'NAME': os.getenv('YTSM_DATABASE_NAME', os.path.join(BASE_DIR, 'ytmanager.db')),
'HOST': os.getenv('YTSM_DATABASE_HOST', None),
'USER': os.getenv('YTSM_DATABASE_USERNAME', None),
'PASSWORD': os.getenv('YTSM_DATABASE_PASSWORD', None),
'PORT': os.getenv('YTSM_DATABASE_PORT', None)
}
}
if os.getenv('YTSM_DATABASE_URL', None):
import dj_database_url
DATABASES['default'] = dj_database_url.parse(os.environ['YTSM_DATABASE_URL'], conn_max_age=600)
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
@ -112,6 +84,9 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login'
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
@ -126,14 +101,126 @@ USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = 'data/media'
# Misc Django stuff
CRISPY_TEMPLATE_PACK = 'bootstrap4'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login'
LOG_FORMAT = '%(asctime)s|%(process)d|%(thread)d|%(name)s|%(filename)s|%(lineno)d|%(levelname)s|%(message)s'
#
# Directories
#
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
PROJECT_ROOT = up(up(os.path.dirname(__file__))) # Project root
BASE_DIR = os.path.join(PROJECT_ROOT, "app") # Base dir of the application
CONFIG_DIR = os.path.join(PROJECT_ROOT, "config")
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
_DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.ini')
_DEFAULT_LOG_FILE = os.path.join(DATA_DIR, 'log.log')
_DEFAULT_MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
DEFAULTS_FILE = os.path.join(CONFIG_DIR, 'defaults.ini')
CONFIG_FILE = os.getenv('YTSM_CONFIG_FILE', _DEFAULT_CONFIG_FILE)
#
# Defaults
#
_DEFAULT_DEBUG = False
_DEFAULT_SECRET_KEY = '^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0'
_DEFAULT_YOUTUBE_API_KEY = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
_DEFAULT_DATABASE = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(DATA_DIR, 'ytmanager.db'),
'HOST': None,
'USER': None,
'PASSWORD': None,
'PORT': None,
}
_SCHEDULER_SYNC_SCHEDULE = '5 * * * *'
_DEFAULT_SCHEDULER_CONCURRENCY = 1
#
# Load globals from config.ini
#
def load_config_ini():
from configparser import ConfigParser
from YtManagerApp.utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv
import dj_database_url
cfg = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolatorWithEnv())
read_ok = cfg.read([DEFAULTS_FILE, CONFIG_FILE])
if DEFAULTS_FILE not in read_ok:
print('Failed to read file ' + DEFAULTS_FILE)
raise Exception('Cannot read file ' + DEFAULTS_FILE)
if CONFIG_FILE not in read_ok:
print('Failed to read file ' + CONFIG_FILE)
raise Exception('Cannot read file ' + CONFIG_FILE)
# Debug
global DEBUG
DEBUG = cfg.getboolean('global', 'Debug', fallback=_DEFAULT_DEBUG)
# Media root, which is where thumbnails are stored
global MEDIA_ROOT
MEDIA_ROOT = cfg.get('global', 'MediaRoot', fallback=_DEFAULT_MEDIA_ROOT)
# Keys - secret key, youtube API key
# SECURITY WARNING: keep the secret key used in production secret!
global SECRET_KEY, YOUTUBE_API_KEY
SECRET_KEY = cfg.get('global', 'SecretKey', fallback=_DEFAULT_SECRET_KEY)
YOUTUBE_API_KEY = cfg.get('global', 'YoutubeApiKey', fallback=_DEFAULT_YOUTUBE_API_KEY)
# Database
global DATABASES
DATABASES = {
'default': _DEFAULT_DATABASE
}
if cfg.has_option('global', 'DatabaseURL'):
DATABASES['default'] = dj_database_url.parse(cfg.get('global', 'DatabaseURL'), conn_max_age=600)
else:
DATABASES['default'] = {
'ENGINE': cfg.get('global', 'DatabaseEngine', fallback=_DEFAULT_DATABASE['ENGINE']),
'NAME': cfg.get('global', 'DatabaseName', fallback=_DEFAULT_DATABASE['NAME']),
'HOST': cfg.get('global', 'DatabaseHost', fallback=_DEFAULT_DATABASE['HOST']),
'USER': cfg.get('global', 'DatabaseUser', fallback=_DEFAULT_DATABASE['USER']),
'PASSWORD': cfg.get('global', 'DatabasePassword', fallback=_DEFAULT_DATABASE['PASSWORD']),
'PORT': cfg.get('global', 'DatabasePort', fallback=_DEFAULT_DATABASE['PORT']),
}
# Log settings
global LOG_LEVEL, LOG_FILE
log_level_str = cfg.get('global', 'LogLevel', fallback='INFO')
try:
LOG_LEVEL = getattr(logging, log_level_str)
except AttributeError:
print("Invalid log level " + LOG_LEVEL)
LOG_LEVEL = logging.INFO
LOG_FILE = cfg.get('global', 'LogFile', fallback=_DEFAULT_LOG_FILE)
# Scheduler settings
global SCHEDULER_SYNC_SCHEDULE, SCHEDULER_CONCURRENCY
SCHEDULER_SYNC_SCHEDULE = cfg.get('global', 'SynchronizationSchedule', fallback=_SCHEDULER_SYNC_SCHEDULE)
SCHEDULER_CONCURRENCY = cfg.getint('global', 'SchedulerConcurrency', fallback=_DEFAULT_SCHEDULER_CONCURRENCY)
load_config_ini()

View File

@ -12,35 +12,26 @@ from django.contrib.auth.models import User
from .models import UserSettings, Subscription
from .utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv
_CONFIG_DIR = os.path.join(dj_settings.BASE_DIR, 'config')
_LOG_FILE = 'log.log'
_LOG_PATH = os.path.join(_CONFIG_DIR, _LOG_FILE)
_LOG_FORMAT = '%(asctime)s|%(process)d|%(thread)d|%(name)s|%(filename)s|%(lineno)d|%(levelname)s|%(message)s'
class AppSettings(ConfigParser):
_DEFAULT_INTERPOLATION = ExtendedInterpolatorWithEnv()
__DEFAULTS_FILE = 'defaults.ini'
__SETTINGS_FILE = 'config.ini'
def __init__(self, *args, **kwargs):
super().__init__(allow_no_value=True, *args, **kwargs)
self.__defaults_path = os.path.join(_CONFIG_DIR, AppSettings.__DEFAULTS_FILE)
self.__settings_path = os.path.join(_CONFIG_DIR, AppSettings.__SETTINGS_FILE)
def initialize(self):
self.read([self.__defaults_path, self.__settings_path])
self.read([dj_settings.DEFAULTS_FILE, dj_settings.CONFIG_FILE])
def save(self):
if os.path.exists(self.__settings_path):
if os.path.exists(dj_settings.CONFIG_FILE):
# Create a backup
copyfile(self.__settings_path, self.__settings_path + ".backup")
copyfile(dj_settings.CONFIG_FILE, dj_settings.CONFIG_FILE + ".backup")
else:
# Ensure directory exists
settings_dir = os.path.dirname(self.__settings_path)
settings_dir = os.path.dirname(dj_settings.CONFIG_FILE)
os.makedirs(settings_dir, exist_ok=True)
with open(self.__settings_path, 'w') as f:
with open(dj_settings.CONFIG_FILE, 'w') as f:
self.write(f)
def __get_combined_dict(self, vars: Optional[Any], sub: Optional[Subscription], user: Optional[User]) -> ChainMap:
@ -112,12 +103,10 @@ def initialize_app_config():
def __initialize_logger():
log_level_str = settings.get('global', 'LogLevel', fallback='INFO')
log_dir = os.path.dirname(dj_settings.LOG_FILE)
os.makedirs(log_dir, exist_ok=True)
try:
log_level = getattr(logging, log_level_str)
logging.basicConfig(filename=_LOG_PATH, level=log_level, format=_LOG_FORMAT)
except AttributeError:
logging.basicConfig(filename=_LOG_PATH, level=logging.INFO, format=_LOG_FORMAT)
logging.warning('Invalid log level "%s" in config file.', log_level_str)
logging.basicConfig(
filename=dj_settings.LOG_FILE,
level=dj_settings.LOG_LEVEL,
format=dj_settings.LOG_FORMAT)

View File

@ -5,10 +5,13 @@ import os
import youtube_dl
import logging
import re
from threading import Lock
log = logging.getLogger('video_downloader')
log_youtube_dl = log.getChild('youtube_dl')
_lock = Lock()
def __get_valid_path(path):
"""
@ -73,27 +76,36 @@ def download_video(video: Video, attempt: int = 1):
log.info('Downloading video %d [%s %s]', video.id, video.video_id, video.name)
max_attempts = settings.getint_sub(video.subscription, 'user', 'DownloadMaxAttempts', fallback=3)
# Issue: if multiple videos are downloaded at the same time, a race condition appears in the mkdirs() call that
# youtube-dl makes, which causes it to fail with the error 'Cannot create folder - file already exists'.
# For now, allow a single download instance.
_lock.acquire()
youtube_dl_params, output_path = __build_youtube_dl_params(video)
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
ret = yt.download(["https://www.youtube.com/watch?v=" + video.video_id])
try:
max_attempts = settings.getint_sub(video.subscription, 'user', 'DownloadMaxAttempts', fallback=3)
log.info('Download finished with code %d', ret)
youtube_dl_params, output_path = __build_youtube_dl_params(video)
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
ret = yt.download(["https://www.youtube.com/watch?v=" + video.video_id])
if ret == 0:
video.downloaded_path = output_path
video.save()
log.info('Video %d [%s %s] downloaded successfully!', video.id, video.video_id, video.name)
log.info('Download finished with code %d', ret)
elif attempt <= max_attempts:
log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts)
__schedule_download_video(video, attempt + 1)
if ret == 0:
video.downloaded_path = output_path
video.save()
log.info('Video %d [%s %s] downloaded successfully!', video.id, video.video_id, video.name)
else:
log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name)
video.downloaded_path = ''
video.save()
elif attempt <= max_attempts:
log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts)
__schedule_download_video(video, attempt + 1)
else:
log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name)
video.downloaded_path = ''
video.save()
finally:
_lock.release()
def __schedule_download_video(video: Video, attempt=1):

View File

@ -35,7 +35,7 @@ class ExtendedInterpolatorWithEnv(Interpolation):
def _resolve_section_option(self, section, option, parser):
if section == 'env':
return os.getenv(option, '')
return parser.get(section, option, raw=True)
return parser.get(section, parser.optionxform(option), raw=True)
def _interpolate_some(self, parser, option, accum, rest, section, map,
depth):
@ -70,7 +70,7 @@ class ExtendedInterpolatorWithEnv(Interpolation):
v = self._resolve_option(opt, map)
elif len(path) == 2:
sect = path[0]
opt = parser.optionxform(path[1])
opt = path[1]
v = self._resolve_section_option(sect, opt, parser)
else:
raise InterpolationSyntaxError(

View File

@ -1,59 +0,0 @@
; Use $<env:environment_variable> to use the value of an environment variable.
; The global section contains settings that apply to the entire server
[global]
; YouTube API key - get this from your user account
;YoutubeApiKey=
; Specifies the synchronization schedule, in crontab format.
; Format: <minute> <hour> <day-of-month> <month-of-year> <day of week>
SynchronizationSchedule=5 * * * *
; Number of threads running the scheduler
; Since most of the jobs scheduled are downloads, there is no advantage to having
; a higher concurrency
SchedulerConcurrency=1
; Log level
LogLevel=DEBUG
; Default user settings
[user]
; When a video is deleted on the system, it will be marked as 'watched'
MarkDeletedAsWatched=True
; Videos marked as watched are automatically deleted
DeleteWatched=True
; Enable automatic downloading
AutoDownload=True
; Limit the total number of videos downloaded (-1 or empty = no limit)
DownloadGlobalLimit=10
; Limit the numbers of videos per subscription (-1 or empty = no limit)
DownloadSubscriptionLimit=5
; Number of download attempts
DownloadMaxAttempts=3
; Download order
; Options: newest, oldest, playlist, playlist_reverse, popularity, rating
DownloadOrder=playlist
; Path where downloaded videos are stored
DownloadPath=data/media/videos
; 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
; The default pattern should work pretty well with Plex
;DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
DownloadFormat=bestvideo+bestaudio
; Subtitles - these options match the youtube-dl options
;DownloadSubtitles=True
;DownloadAutogeneratedSubtitles=False
;DownloadSubtitlesAll=False
;DownloadSubtitlesLangs=en,ro
;DownloadSubtitlesFormat=

View File

@ -1,59 +0,0 @@
; Use $<env:environment_variable> to use the value of an environment variable.
; The global section contains settings that apply to the entire server
[global]
; YouTube API key - get this from your user account
YoutubeApiKey=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8
; Specifies the synchronization schedule, in crontab format.
; Format: <minute> <hour> <day-of-month> <month-of-year> <day of week>
SynchronizationSchedule=0 * * * *
; Number of threads running the scheduler
; Since most of the jobs scheduled are downloads, there is no advantage to having
; a higher concurrency
SchedulerConcurrency=2
; Log level
LogLevel=INFO
; Default user settings
[user]
; When a video is deleted on the system, it will be marked as 'watched'
MarkDeletedAsWatched=True
; Videos marked as watched are automatically deleted
DeleteWatched=True
; Enable automatic downloading
AutoDownload=True
; Limit the total number of videos downloaded (-1 or empty = no limit)
DownloadGlobalLimit=
; Limit the numbers of videos per subscription (-1 or empty = no limit)
DownloadSubscriptionLimit=5
; Number of download attempts
DownloadMaxAttempts=3
; Download order
; Options: newest, oldest, playlist, playlist_reverse, popularity, rating
DownloadOrder=playlist
; Path where downloaded videos are stored
DownloadPath=${env:USERPROFILE}${env:HOME}/Downloads
; 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
; The default pattern should work pretty well with Plex
DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
DownloadFormat=bestvideo+bestaudio
; Subtitles - these options match the youtube-dl options
DownloadSubtitles=True
DownloadAutogeneratedSubtitles=False
DownloadSubtitlesAll=False
DownloadSubtitlesLangs=en,ro
DownloadSubtitlesFormat=

View File

@ -1,47 +1,75 @@
; Use $<env:environment_variable> to use the value of an environment variable.
; Use ${env:environment_variable} to use the value of an environment variable.
; If a variable is not set here, it will be loaded from defaults.ini.
; The global section contains settings that apply to the entire server
[global]
Debug=${env:YTSM_DEBUG}
; This is the folder where thumbnails will be downloaded. By default project_root/data/media is used.
;MediaRoot=
; Secret key - django secret key
;SecretKey=^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0
; YouTube API key - get this from your user account
YoutubeApiKey=AIzaSyAonB6T-DrKjfGxBGuHyFMg0x_d0T9nlP8
;YoutubeApiKey=AIzaSyAonB6T-DrKjfGxBGuHyFMg0x_d0T9nlP8
; Database settings
; You can use any database engine supported by Django, as long as you add the required dependencies.
; Built-in engines: https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-DATABASE-ENGINE
; Others databases might be supported by installing the corect pip package.
;DatabaseEngine=django.db.backends.sqlite3
;DatabaseName=data/ytmanager.db
;DatabaseHost=
;DatabaseUser=
;DatabasePassword=
;DatabasePort=
; Database one-liner. If set, it will override any other Database* setting.
; Documentation: https://github.com/kennethreitz/dj-database-url
;DatabaseURL=sqlite:////full/path/to/your/database/file.sqlite
; Log settings, sets the log file location and the log level
;LogLevel=INFO
;LogFile=data/log.log
; Specifies the synchronization schedule, in crontab format.
; Format: <minute> <hour> <day-of-month> <month-of-year> <day of week>
SynchronizationSchedule=5 * * * *
;SynchronizationSchedule=5 * * * *
; Number of threads running the scheduler
; Since most of the jobs scheduled are downloads, there is no advantage to having
; a higher concurrency
SchedulerConcurrency=1
; Log level
LogLevel=DEBUG
;SchedulerConcurrency=1
; Default user settings
[user]
; When a video is deleted on the system, it will be marked as 'watched'
MarkDeletedAsWatched=True
;MarkDeletedAsWatched=True
; Videos marked as watched are automatically deleted
DeleteWatched=True
;DeleteWatched=True
; Enable automatic downloading
AutoDownload=True
;AutoDownload=True
; Limit the total number of videos downloaded (-1 or empty = no limit)
DownloadGlobalLimit=10
;DownloadGlobalLimit=10
; Limit the numbers of videos per subscription (-1 or empty = no limit)
DownloadSubscriptionLimit=5
;DownloadSubscriptionLimit=5
; Number of download attempts
DownloadMaxAttempts=3
;DownloadMaxAttempts=3
; Download order
; Options: newest, oldest, playlist, playlist_reverse, popularity, rating
DownloadOrder=playlist
;DownloadOrder=playlist
; Path where downloaded videos are stored
DownloadPath=data/media/videos
DownloadPath=${env:YTSM_DATA_PATH}/videos
; 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
@ -49,7 +77,7 @@ DownloadPath=data/media/videos
;DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
DownloadFormat=bestvideo+bestaudio
;DownloadFormat=bestvideo+bestaudio
; Subtitles - these options match the youtube-dl options
;DownloadSubtitles=True

View File

@ -1,20 +1,48 @@
; Use $<env:environment_variable> to use the value of an environment variable.
; Use ${env:environment_variable} to use the value of an environment variable.
; The global section contains settings that apply to the entire server
[global]
; Controls whether django debug mode is enabled. Should be false in production.
Debug=False
; This is the folder where thumbnails will be downloaded. By default project_root/data/media is used.
;MediaRoot=
; Secret key - django secret key
SecretKey=^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0
; YouTube API key - get this from your user account
YoutubeApiKey=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8
; Database settings
; You can use any database engine supported by Django, as long as you add the required dependencies.
; Built-in engines: https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-DATABASE-ENGINE
; Others databases might be supported by installing the corect pip package.
;DatabaseEngine=django.db.backends.sqlite3
;DatabaseName=data/ytmanager.db
;DatabaseHost=
;DatabaseUser=
;DatabasePassword=
;DatabasePort=
; Database one-liner. If set, it will override any other Database* setting.
; Documentation: https://github.com/kennethreitz/dj-database-url
;DatabaseURL=sqlite:////full/path/to/your/database/file.sqlite
; Log settings, sets the log file location and the log level
LogLevel=INFO
; LogFile=data/log.log
; Specifies the synchronization schedule, in crontab format.
; Format: <minute> <hour> <day-of-month> <month-of-year> <day of week>
SynchronizationSchedule=0 * * * *
SynchronizationSchedule=5 * * * *
; Number of threads running the scheduler
; Since most of the jobs scheduled are downloads, there is no advantage to having
; a higher concurrency
SchedulerConcurrency=2
SchedulerConcurrency=3
; Log level
LogLevel=INFO
; Default user settings
[user]
@ -41,7 +69,7 @@ DownloadMaxAttempts=3
DownloadOrder=playlist
; Path where downloaded videos are stored
DownloadPath=${env:USERPROFILE}${env:HOME}/Downloads
DownloadPath=data/videos
; 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

View File

@ -4,8 +4,9 @@ services:
nginx:
image: nginx:latest
volumes:
- ./nginx:/etc/nginx/conf.d/
- ./docker/nginx:/etc/nginx/conf.d/
- ./app/YtManagerApp/static:/www/static
- ./data/media:/www/media
ports:
- "80:80"
depends_on:
@ -13,11 +14,9 @@ services:
web:
build: .
env_file:
- sqlite3.env.env
tty: true
ports:
- "8000:8000"
volumes:
- ./media:/usr/src/app/data/media
- ./db:/usr/src/app/data/db
- ./config:/usr/src/ytsm/config
- ./data:/usr/src/ytsm/data

View File

@ -1,5 +1,4 @@
#!/bin/bash
#./manage.py runserver 0.0.0.0:8000 --noreload
./manage.py migrate
gunicorn -b 0.0.0.0:8000 -w 4 YtManager.wsgi

View File

@ -15,6 +15,10 @@ server {
alias /www/static;
expires 30d;
}
location /media {
alias /www/media;
expires 30d;
}
location / {
try_files $uri @proxy_to_app;

View File

@ -1,3 +0,0 @@
YTSM_DATABASE_ENGINE=django.db.backends.sqlite3
YTSM_DATABASE_NAME=/usr/src/app/data/db/ytmanager.db
YTSM_YOUTUBE_API_KEY=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8