diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 894a44c..7be2372
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,6 @@ venv.bak/
# mypy
.mypy_cache/
+
+# keys folder
+keys/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100755
index 0000000..7005c2c
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# https://hub.docker.com/r/library/python/tags/
+image: python:latest
+
+# Change pip's cache directory to be inside the project directory since we can
+# only cache local items.
+variables:
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+
+# Pip's cache doesn't store the python packages
+# https://pip.pypa.io/en/stable/reference/pip_install/#caching
+#
+# If you want to also cache the installed packages, you have to install
+# them in a virtualenv and cache it as well.
+cache:
+ paths:
+ - .cache/pip
+ - venv/
+
+before_script:
+ - python -V # Print out python version for debugging
+ - apt update && apt upgrade -y
+ - apt install -y libchromaprint-tools ffmpeg --install-recommends
+ - pip install -r YouTubeMDBot/requirements.txt
+
+test:pylint:
+ script:
+ - pip install pylint
+ - pylint -j 0 --exit-zero --ignored-classes=_socketobject *.py YouTubeMDBot
+
+test:
+ script:
+ - python -m unittest $(pwd)/YouTubeMDBot/tests/*.py
diff --git a/Design/Database/YouTubeMDBot.drawio b/Design/Database/YouTubeMDBot.drawio
new file mode 100644
index 0000000..6e7c827
--- /dev/null
+++ b/Design/Database/YouTubeMDBot.drawio
@@ -0,0 +1 @@
+7V1Nl5s2FP01XjaHb5vlfGSaTdq0c3qarObIINt0AE1AZMb59RVGwraEOwyWkZgqmxjBCNC9enq670nM3Jvs5dcCPG0+oximM8eKX2bu7cxxbNddkP/qkm1TsrCcpmBdJDG9aF9wn/yEtNCipVUSw/LoQoxQipOn48II5TmM8FEZKAr0fHzZCqXHd30CaygU3EcgFUv/TmK8oW/hBPvyTzBZb9id7SBszixB9LguUJXT+80cd7X715zOAKuLvmi5ATF6PihyP87cmwIh3PzKXm5gWrcta7bm7+5OnG2fu4A57vMHle/e383/zLIAeD+j4J8s+eP3X2yXPt0PkFa0Rf4qYUEfGW9ZM+1eFNZV2TP3+nmTYHj/BKL67DMhBinb4Cylp9tX3R3gAj3CG5SiYleVa1muGwTkzCpJ04PyW++jfefX5SjHXdeDNFnnpCwib0we0b1ep6Cs4bfI7whlSUR/1xXc02evHyEFS5het3ixunOUw/YsKmJYcGd29TSMtT1yTJsKFhi+nETBbrElfQaiDOJiSy6hf+Ay3tPuYof0+HlPPptRZnNAvICWAcr3dVv1HnPyg8L+JgrYAgVmTpCS+15X5Me6/kFqoEXkHm2pwBJyO9Jv4esMOabTEV9OgfrfIA5j2SG4vVkngQU8CXpyYHE5DjgdHODBzeOr2uDuG/0A0L6t2dQKY8Eoc01H7oyqIoJ9bBcGxRri1ykuwnHQ4H5He7OyAqYAJz+OH7kLBHqHLyghL3MKbaErN29K/+jQgnP1BFw9NldP0w5CPTtGtC99DklcgSQ5yKCxAkOtQKCdGfAEhDFYG4BlDfbqAfYFgFOQG4SHIuwFvm4IBwLCq6Qo8QOIIkjwMEgPQzpwtOvL8/fts7ka+WyuI8lnm4/tsy1GIgl8SfDXut9/8OnRN2oF6t+3L4cHW3qgiFieIZYEYoXv2/r4hiTnk8QT1cV3RZLAkEQCSU7rj0umNH5KSozqxmhFyOVJEXK4VP1/8GP10589UXuMATay0mCE/ROGQd1UxROVQyHEQJoSPpg4wyU7uw5MEBVG/T0CNkK97hE0tkwTj8DnR/KBHoHPexYX9whEZWMDROmq2KBsWZU6WIF4Hi4tS7QCqxUMoqiDthfp7QtubF+I3X0xbm8fTX3IyfN+pdjtDnbqQ6NF1Id7AWJ3dAkFgrG2x7zB6sZRiZXwePgHWgnPHdtKTFGBeMNQMjckOZ8krDkOSEKgv2J47ZlCzC7mpoZHRp6lBB3Yd1okmPLaiCcRSK/oiSyJ4/SUs7ofmyw5Y9PbWH3WgNNKBUw6sMQBx+vgKO9PyBtwfFFLIC129ZuBW4Z/4WoHtygdTGAIcHsPATqlLU13CDgtPrQi4pcCriDBMoKl0RjPNhS+dQy564uGYmSR0Rd1BzOl7IEkn/WgfErpixlM+pv8/iElX2lw2vO4jusNtPleyLkK/thGX0yD0p8mzEj1oInS5Bg+F8od7BpwEwh3dN9AVBrN9FDWfMHTb74g6pEGbllw875C25nVwS0qhKCKE/SwQkUGsDggmLBiv1xYvmOrjyuymYMA9feKtADeGqwHYj3n53LqE58DUeQriS/3UKJ8/ZAm+aMBWxLYngZgi+oNKB9rC/5A7gBigIGBW5Id9y1POdxTzA9hfobu8zZO0Q0kLUQNxl6IGkxRA2KNZkgyEkmmqAAx18aQZCSSTHENXdA7gGhIIoUkXVlsXADxc+uLmujhuVklnvPheFV1wJatq4sfBqKUZDbJuTAPTmQhq5ucsGjF4fYYCU7NUhVJEM81gFjUlkCBk9KIxbJ2ufE6UgjGBtkRQU6XVWYwHoix6+iHsagbRuhHx86GBuO+W93oNx6LYiGZskBQNgsJDdDDgPb1G5VFwa+AESriJF8bqOXZ7flcPdSibBdXBcAJyg3MklwwHWAWhbeoKjHKTFRP8qQq0MB8iwLaFlW4WppxWuLegzoAPcUlmfPe63YbIVgTOT2UtN/PYuz9fhj9pkWS3tFbQxIpJOnaFEp7kgSGJKOSRNTxJkAS35BkVJJ0Lf/UniS9VwIZkkghyRTTDtm0ypBkJJJMMu2wP0lClSQRZIuhS0/5TUn8cGyaTDHxkMW9dbclLrdCIRhqTFwuyiFslndxmkwy9bC3NWli7Mpowuce8guLe9OEj3ry4tnFaTLWFnqKaKI0Q9WdSxp0PD5ZbfRB53R+YpuHepfUqWomP1V6fmrYN93lcvmpjG8mP3XExZLHJFAfVA07khfNImgJUPPmXQesHQHrsm4dA7Esq25b6rMYQ1G8NNuW9QAz4MFcBCKW425cFk5RYmRu5euOfKjTUjPbGTrf4z/Jao893wvHEhn3W6bvd0n/RusYa8v0sPdKRtWqk8tZk/kwegkJl/yE8+L0mqI4ySY3r9NkoVTDtrldD21r6JJXx+NrGnvRazhFeZI5xdoThZ9PDNWdbG4SuhhbdwqnKE+yebL2NHk/Xo0oT5qNF2VtvOhagiDZMdsZdevFdqtPA/hFAOe8SNUbqzpWV7omv8d+CrbpbiW1CUGcu3Ebv6BHeQDCsUQ90gQgxmVBwL6kqkyvdKwJJlu2xutVd5CS3LiDZ7qDjtWlhXLDxT0G9b4bSWS+yCLBVHA7faqPWDuWKHTiJINicMMMB8Mw1iB+5VgTlBtbZvYYD5R+reUdjQcnPvhrLSHMdw31vYIlJn1bMPsm8nlsBLi01oXqDzY51gQVwtZB6WECdPpM65RNgFEILycYBVxgSr0+yIg7LavQd6cAOqU0VuFcq2BPcBl4f5oozmrgP9MZ8jHmvjTxeaeDnzWeoAmBDWwPLnuqLyhPPzD/VcEFtU971jU1SubgJDWt3hzUK8TZhjbONVV9vzj5Zg7ySsp5HCSHBUL48HLibmw+oxjWV/wL
\ No newline at end of file
diff --git a/Design/Database/YouTubeMDBot.pdf b/Design/Database/YouTubeMDBot.pdf
new file mode 100644
index 0000000..3754129
Binary files /dev/null and b/Design/Database/YouTubeMDBot.pdf differ
diff --git a/Design/Database/YouTubeMDBot.png b/Design/Database/YouTubeMDBot.png
new file mode 100644
index 0000000..c003094
Binary files /dev/null and b/Design/Database/YouTubeMDBot.png differ
diff --git a/Design/Database/YouTubeMDBot.svg b/Design/Database/YouTubeMDBot.svg
new file mode 100644
index 0000000..95d49cb
--- /dev/null
+++ b/Design/Database/YouTubeMDBot.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/Design/Database/database_model.mwb b/Design/Database/database_model.mwb
deleted file mode 100644
index 03ffdc9..0000000
Binary files a/Design/Database/database_model.mwb and /dev/null differ
diff --git a/Design/Database/database_model.mwb.bak b/Design/Database/database_model.mwb.bak
deleted file mode 100644
index ea968e3..0000000
Binary files a/Design/Database/database_model.mwb.bak and /dev/null differ
diff --git a/Design/Database/generated_sql_file.sql b/Design/Database/generated_sql_file.sql
deleted file mode 100644
index e65336e..0000000
--- a/Design/Database/generated_sql_file.sql
+++ /dev/null
@@ -1,251 +0,0 @@
--- MySQL Script generated by MySQL Workbench
--- lun 22 jul 2019 14:28:48 CEST
--- Model: New Model Version: 1.0
--- MySQL Workbench Forward Engineering
-
-SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
-SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
-SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
-
--- -----------------------------------------------------
--- Schema youtubemd
--- -----------------------------------------------------
-
--- -----------------------------------------------------
--- Schema youtubemd
--- -----------------------------------------------------
-CREATE SCHEMA IF NOT EXISTS `youtubemd` DEFAULT CHARACTER SET utf8mb4 ;
-SHOW WARNINGS;
-USE `youtubemd` ;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`User`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`User` (
- `id` INT(64) NOT NULL DEFAULT 0,
- `name` VARCHAR(45) NULL DEFAULT 'User',
- `surname` VARCHAR(45) NULL,
- `username` VARCHAR(45) NULL,
- `lastSeen` DATETIME NOT NULL,
- `firstUsage` DATETIME NOT NULL,
- PRIMARY KEY (`id`))
-ENGINE = InnoDB
-CHECKSUM = 1
-PACK_KEYS = 1;
-
-SHOW WARNINGS;
-CREATE UNIQUE INDEX `id_UNIQUE` ON `youtubemd`.`User` (`id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-CREATE UNIQUE INDEX `username_UNIQUE` ON `youtubemd`.`User` (`username` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`Preferences`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`Preferences` (
- `language` VARCHAR(3) NOT NULL DEFAULT 'en',
- `audioQuality` ENUM('320k', '256k', '128k') NOT NULL DEFAULT '128k',
- `audioSampling` ENUM('44000', '48000') NOT NULL DEFAULT '44000',
- `sendSongLinks` TINYINT NOT NULL DEFAULT 0,
- `User_id` INT(64) NOT NULL,
- PRIMARY KEY (`User_id`),
- CONSTRAINT `fk_Preferences_User`
- FOREIGN KEY (`User_id`)
- REFERENCES `youtubemd`.`User` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`Metadata`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`Metadata` (
- `idMetadata` INT NOT NULL AUTO_INCREMENT,
- `title` VARCHAR(100) NOT NULL,
- `artist` VARCHAR(60) NOT NULL,
- `cover` BLOB NOT NULL,
- `duration` INT NULL,
- `customMetadata` TINYINT NOT NULL DEFAULT 0,
- PRIMARY KEY (`idMetadata`))
-ENGINE = InnoDB
-AUTO_INCREMENT = 0
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`VideoInformation`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`VideoInformation` (
- `id` VARCHAR(11) NOT NULL,
- `title` VARCHAR(100) NOT NULL,
- `channel` VARCHAR(60) NOT NULL,
- PRIMARY KEY (`id`))
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-CREATE UNIQUE INDEX `id_UNIQUE` ON `youtubemd`.`VideoInformation` (`id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`DownloadInformation`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`DownloadInformation` (
- `file_id` VARCHAR(50) NOT NULL,
- `audioQuality` ENUM('320k', '256k', '128k') NOT NULL,
- `audioSampling` ENUM('44000', '48000') NULL,
- `Metadata_idMetadata` INT NOT NULL,
- `VideoInformation_id` VARCHAR(11) NOT NULL,
- PRIMARY KEY (`file_id`, `Metadata_idMetadata`, `VideoInformation_id`),
- CONSTRAINT `fk_DownloadInformation_Metadata1`
- FOREIGN KEY (`Metadata_idMetadata`)
- REFERENCES `youtubemd`.`Metadata` (`idMetadata`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION,
- CONSTRAINT `fk_DownloadInformation_VideoInformation1`
- FOREIGN KEY (`VideoInformation_id`)
- REFERENCES `youtubemd`.`VideoInformation` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-CREATE UNIQUE INDEX `file_id_UNIQUE` ON `youtubemd`.`DownloadInformation` (`file_id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-CREATE INDEX `fk_DownloadInformation_Metadata1_idx` ON `youtubemd`.`DownloadInformation` (`Metadata_idMetadata` ASC) VISIBLE;
-
-SHOW WARNINGS;
-CREATE INDEX `fk_DownloadInformation_VideoInformation1_idx` ON `youtubemd`.`DownloadInformation` (`VideoInformation_id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`History`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`History` (
- `User_id` INT(64) NOT NULL,
- `DownloadInformation_file_id` VARCHAR(50) NOT NULL,
- PRIMARY KEY (`User_id`, `DownloadInformation_file_id`),
- CONSTRAINT `fk_History_User1`
- FOREIGN KEY (`User_id`)
- REFERENCES `youtubemd`.`User` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION,
- CONSTRAINT `fk_History_DownloadInformation1`
- FOREIGN KEY (`DownloadInformation_file_id`)
- REFERENCES `youtubemd`.`DownloadInformation` (`file_id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-CREATE INDEX `fk_History_DownloadInformation1_idx` ON `youtubemd`.`History` (`DownloadInformation_file_id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`VideoStatistics`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`VideoStatistics` (
- `timesRequested` INT NOT NULL DEFAULT 0,
- `VideoInformation_id` VARCHAR(11) NOT NULL,
- PRIMARY KEY (`VideoInformation_id`),
- CONSTRAINT `fk_VideoStatistics_VideoInformation1`
- FOREIGN KEY (`VideoInformation_id`)
- REFERENCES `youtubemd`.`VideoInformation` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`DownloadStatistics`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`DownloadStatistics` (
- `timesRequested` INT NOT NULL DEFAULT 0,
- `DownloadInformation_file_id` VARCHAR(50) NOT NULL,
- PRIMARY KEY (`DownloadInformation_file_id`),
- CONSTRAINT `fk_DownloadStatistics_DownloadInformation1`
- FOREIGN KEY (`DownloadInformation_file_id`)
- REFERENCES `youtubemd`.`DownloadInformation` (`file_id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`Playlist`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`Playlist` (
- `id` VARCHAR(60) NOT NULL,
- PRIMARY KEY (`id`))
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-CREATE UNIQUE INDEX `id_UNIQUE` ON `youtubemd`.`Playlist` (`id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`Playlist_has_VideoInformation`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`Playlist_has_VideoInformation` (
- `Playlist_id` VARCHAR(60) NOT NULL,
- `VideoInformation_id` VARCHAR(11) NOT NULL,
- PRIMARY KEY (`Playlist_id`, `VideoInformation_id`),
- CONSTRAINT `fk_Playlist_has_VideoInformation_Playlist1`
- FOREIGN KEY (`Playlist_id`)
- REFERENCES `youtubemd`.`Playlist` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION,
- CONSTRAINT `fk_Playlist_has_VideoInformation_VideoInformation1`
- FOREIGN KEY (`VideoInformation_id`)
- REFERENCES `youtubemd`.`VideoInformation` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-CREATE INDEX `fk_Playlist_has_VideoInformation_VideoInformation1_idx` ON `youtubemd`.`Playlist_has_VideoInformation` (`VideoInformation_id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-CREATE INDEX `fk_Playlist_has_VideoInformation_Playlist1_idx` ON `youtubemd`.`Playlist_has_VideoInformation` (`Playlist_id` ASC) VISIBLE;
-
-SHOW WARNINGS;
-
--- -----------------------------------------------------
--- Table `youtubemd`.`PlaylistStatistics`
--- -----------------------------------------------------
-CREATE TABLE IF NOT EXISTS `youtubemd`.`PlaylistStatistics` (
- `timesRequested` INT NOT NULL,
- `Playlist_id` VARCHAR(60) NOT NULL,
- PRIMARY KEY (`Playlist_id`),
- CONSTRAINT `fk_PlaylistStatistics_Playlist1`
- FOREIGN KEY (`Playlist_id`)
- REFERENCES `youtubemd`.`Playlist` (`id`)
- ON DELETE NO ACTION
- ON UPDATE NO ACTION)
-ENGINE = InnoDB
-CHECKSUM = 1;
-
-SHOW WARNINGS;
-
-SET SQL_MODE=@OLD_SQL_MODE;
-SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
-SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
diff --git a/Design/Database/img_database_model.png b/Design/Database/img_database_model.png
deleted file mode 100644
index 6046766..0000000
Binary files a/Design/Database/img_database_model.png and /dev/null differ
diff --git a/Design/Database/new_database_model.mwb b/Design/Database/new_database_model.mwb
new file mode 100644
index 0000000..8a3992e
Binary files /dev/null and b/Design/Database/new_database_model.mwb differ
diff --git a/Design/Database/pdf_database_model.pdf b/Design/Database/pdf_database_model.pdf
deleted file mode 100644
index 4cdb445..0000000
Binary files a/Design/Database/pdf_database_model.pdf and /dev/null differ
diff --git a/Design/Database/psql_model.sql b/Design/Database/psql_model.sql
new file mode 100644
index 0000000..7fffb89
--- /dev/null
+++ b/Design/Database/psql_model.sql
@@ -0,0 +1,266 @@
+-- PostgreSQL model for YouTubeMDBot application
+-- Created by Javinator9889 - thu, 24 October, 2019
+-- Last modification: mon, 4 November, 2019
+-- Version 1.1
+
+-- DROP schema - only for testing
+-- DROP SCHEMA IF EXISTS youtubemd CASCADE;
+-- DROP TYPE IF EXISTS AFORMAT;
+-- DROP TYPE IF EXISTS aquality;
+-- DROP TYPE IF EXISTS behaviour;
+
+-- Custom "enum" types
+CREATE TYPE AFORMAT AS ENUM ('mp3', 'm4a', 'ogg');
+CREATE TYPE AQUALITY AS ENUM ('128k', '96k');
+CREATE TYPE BEHAVIOUR AS ENUM ('always', 'not_found', 'ask', 'never');
+
+-- Create DB schema
+CREATE SCHEMA IF NOT EXISTS youtubemd;
+
+-- ---------------------------------------
+-- Table User --
+-- ---------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.User
+(
+ "id" INT PRIMARY KEY NOT NULL,
+ "name" VARCHAR(45),
+ "tag" VARCHAR(45),
+ "lang" VARCHAR(2),
+ "first_access" date
+);
+
+-- ---------------------------------------------
+-- Table Preferences --
+-- ---------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.Preferences
+(
+ "audio_format" AFORMAT NOT NULL DEFAULT 'm4a',
+ "audio_quality" AQUALITY NOT NULL DEFAULT '128k',
+ "song_behaviour" BEHAVIOUR NOT NULL DEFAULT 'not_found',
+ "send_song_link" BOOLEAN DEFAULT False,
+ "user_id" INT NOT NULL,
+ PRIMARY KEY ("user_id"),
+ CONSTRAINT "fk_user_id"
+ FOREIGN KEY ("user_id")
+ REFERENCES youtubemd.User ("id")
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+-- ------------------------------------------
+-- Table YouTube --
+-- ------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.YouTube
+(
+ "id" VARCHAR(11) UNIQUE NOT NULL,
+ "times_requested" INT NOT NULL DEFAULT 0,
+ PRIMARY KEY ("id")
+);
+
+-- ------------------------------------------
+-- Table Metadata --
+-- ------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.Metadata
+(
+ "id" BIGSERIAL NOT NULL,
+ "artist" VARCHAR(45) NOT NULL,
+ "album" VARCHAR(45) NOT NULL,
+ "cover" BYTEA NOT NULL,
+ "release_id" VARCHAR(36),
+ "recording_id" VARCHAR(36),
+ "duration" INT,
+ "title" VARCHAR(45),
+ "custom_metadata" BOOLEAN,
+ PRIMARY KEY ("id")
+);
+
+-- ----------------------------------------------------
+-- Relation between YouTube and Metadata --
+-- ----------------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.Video_Has_Metadata
+(
+ "id" VARCHAR(11) NOT NULL,
+ "metadata_id" INT NOT NULL,
+ PRIMARY KEY ("id", "metadata_id"),
+ CONSTRAINT "fk_video_id"
+ FOREIGN KEY ("id")
+ REFERENCES youtubemd.YouTube ("id"),
+ CONSTRAINT "fk_metadata_id"
+ FOREIGN KEY ("metadata_id")
+ REFERENCES youtubemd.Metadata ("id")
+);
+
+-- --------------------------------------
+-- Table File --
+-- --------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.File
+(
+ "id" VARCHAR(50) UNIQUE NOT NULL,
+ "metadata_id" INT UNIQUE NOT NULL,
+ "audio_quality" AQUALITY NOT NULL,
+ "size" INT,
+ PRIMARY KEY ("id", "metadata_id"),
+ CONSTRAINT "fk_metadata_id"
+ FOREIGN KEY ("metadata_id")
+ REFERENCES youtubemd.Metadata ("id")
+);
+
+-- -----------------------------------------
+-- Table History --
+-- -----------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.History
+(
+ "id" BIGSERIAL NOT NULL,
+ "file_id" VARCHAR(50) NOT NULL,
+ "user_id" INT NOT NULL,
+ "metadata_id" INT NOT NULL,
+ "date" date,
+ PRIMARY KEY ("id", "file_id", "user_id", "metadata_id"),
+ CONSTRAINT "fk_user_id"
+ FOREIGN KEY ("user_id")
+ REFERENCES youtubemd.User ("id")
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT "fk_file_id"
+ FOREIGN KEY ("file_id")
+ REFERENCES youtubemd.File ("id"),
+ CONSTRAINT "fk_metadata_id"
+ FOREIGN KEY ("metadata_id")
+ REFERENCES youtubemd.Metadata ("id")
+);
+
+-- ------------------------------------------
+-- Table Playlist --
+-- ------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.Playlist
+(
+ "id" VARCHAR(22) NOT NULL UNIQUE,
+ PRIMARY KEY ("id")
+);
+
+-- ----------------------------------------------
+-- Table YouTube stats --
+-- ----------------------------------------------
+CREATE TABLE IF NOT EXISTS youtubemd.YouTubeStats
+(
+ "id" VARCHAR(11) NOT NULL UNIQUE,
+ "daily_requests" INT NOT NULL DEFAULT 1,
+ "weekly_requests" INT NOT NULL DEFAULT 1,
+ "monthly_requests" INT NOT NULL DEFAULT 1,
+ PRIMARY KEY ("id"),
+ CONSTRAINT "fk_youtube_id"
+ FOREIGN KEY ("id")
+ REFERENCES youtubemd.YouTube ("id")
+);
+
+-- Additional indexes
+CREATE INDEX youtubemd.user_preferences_ix ON youtubemd.Preferences ("user_id");
+CREATE INDEX youtubemd.video_metadata_ix ON youtubemd.Video_Has_Metadata ("id", "metadata_id");
+CREATE INDEX youtubemd.history_ix ON youtubemd.History ("id", "file_id", "user_id", "metadata_id");
+
+-- Trigger that updated the different stats
+CREATE FUNCTION youtubemd.process_stats() RETURNS trigger AS
+$$
+DECLARE
+ daily_value INT;
+ weekly_value INT;
+ monthly_value INT;
+BEGIN
+ IF (SELECT EXISTS(SELECT 1 FROM youtubemd.YouTubeStats WHERE youtubemd.YouTubeStats.id = NEW.id)) THEN
+ SELECT INTO daily_value, weekly_value, monthly_value youtubemd.YouTubeStats.daily_requests,
+ youtubemd.YouTubeStats.weekly_requests,
+ youtubemd.YouTubeStats.monthly_requests
+ FROM youtubemd.YouTubeStats
+ WHERE youtubemd.YouTubeStats.id = NEW.id;
+ daily_value = daily_value + 1;
+ weekly_value = weekly_value + 1;
+ monthly_value = monthly_value + 1;
+
+ UPDATE youtubemd.YouTubeStats
+ SET daily_requests = daily_value,
+ weekly_requests = weekly_value,
+ monthly_requests = monthly_value
+ WHERE id = NEW.id;
+ ELSE
+ INSERT INTO youtubemd.YouTubeStats (id) VALUES (NEW.id);
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Complementary functions with useful operations
+CREATE FUNCTION youtubemd.top_10_daily()
+ RETURNS TABLE
+ (
+ id VARCHAR(11),
+ daily_requests INT
+ )
+AS
+$$
+BEGIN
+ RETURN QUERY SELECT DISTINCT youtubemd.YouTubeStats.id, youtubemd.YouTubeStats.daily_requests
+ FROM youtubemd.youtubestats
+ ORDER BY daily_requests DESC
+ FETCH FIRST 10 ROWS ONLY;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION youtubemd.top_10_weekly()
+ RETURNS TABLE
+ (
+ id VARCHAR(11),
+ weekly_requests INT
+ )
+AS
+$$
+BEGIN
+ RETURN QUERY SELECT DISTINCT youtubemd.YouTubeStats.id, youtubemd.YouTubeStats.weekly_requests
+ FROM youtubemd.youtubestats
+ ORDER BY weekly_requests DESC
+ FETCH FIRST 10 ROWS ONLY;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION youtubemd.top_10_monthly()
+ RETURNS TABLE
+ (
+ id VARCHAR(11),
+ monthly_requests INT
+ )
+AS
+$$
+BEGIN
+ RETURN QUERY SELECT DISTINCT youtubemd.YouTubeStats.id, youtubemd.YouTubeStats.monthly_requests
+ FROM youtubemd.youtubestats
+ ORDER BY monthly_requests DESC
+ FETCH FIRST 10 ROWS ONLY;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION youtubemd.clear_daily_stats() AS
+$$
+BEGIN
+ UPDATE youtubemd.YouTubeStats SET daily_requests = 0;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION youtubemd.clear_weekly_stats() AS
+$$
+BEGIN
+ UPDATE youtubemd.YouTubeStats SET weekly_requests = 0;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE FUNCTION youtubemd.clear_monthly_stats() AS
+$$
+BEGIN
+ UPDATE youtubemd.YouTubeStats SET monthly_requests = 0;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Init the trigger
+CREATE TRIGGER stats_update
+ AFTER INSERT OR UPDATE
+ ON youtubemd.YouTube
+ FOR EACH ROW
+EXECUTE PROCEDURE youtubemd.process_stats();
diff --git a/Design/Database/vect_database_model.svg b/Design/Database/vect_database_model.svg
deleted file mode 100644
index fe40b7a..0000000
--- a/Design/Database/vect_database_model.svg
+++ /dev/null
@@ -1,3111 +0,0 @@
-
-
diff --git a/Design/SRS/Software Requirement Specification.docx b/Design/SRS/Software Requirement Specification.docx
old mode 100644
new mode 100755
diff --git a/Design/SRS/Software Requirement Specification.pdf b/Design/SRS/Software Requirement Specification.pdf
old mode 100644
new mode 100755
diff --git a/Design/logo/logo.png b/Design/logo/logo.png
old mode 100644
new mode 100755
diff --git a/Design/logo/logo.psd b/Design/logo/logo.psd
old mode 100644
new mode 100755
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
diff --git a/YouTubeMDBot/.idea/.gitignore b/YouTubeMDBot/.idea/.gitignore
new file mode 100755
index 0000000..0e40fe8
--- /dev/null
+++ b/YouTubeMDBot/.idea/.gitignore
@@ -0,0 +1,3 @@
+
+# Default ignored files
+/workspace.xml
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/YouTubeMDBot.iml b/YouTubeMDBot/.idea/YouTubeMDBot.iml
new file mode 100755
index 0000000..6711606
--- /dev/null
+++ b/YouTubeMDBot/.idea/YouTubeMDBot.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/codeStyles/codeStyleConfig.xml b/YouTubeMDBot/.idea/codeStyles/codeStyleConfig.xml
new file mode 100755
index 0000000..a55e7a1
--- /dev/null
+++ b/YouTubeMDBot/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/dictionaries/javinator9889.xml b/YouTubeMDBot/.idea/dictionaries/javinator9889.xml
new file mode 100755
index 0000000..787b6ce
--- /dev/null
+++ b/YouTubeMDBot/.idea/dictionaries/javinator9889.xml
@@ -0,0 +1,10 @@
+
+
+
+ acoustid
+ ffmpeg
+ fpcalc
+ javinator
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/git_toolbox_prj.xml b/YouTubeMDBot/.idea/git_toolbox_prj.xml
new file mode 100644
index 0000000..c7846c0
--- /dev/null
+++ b/YouTubeMDBot/.idea/git_toolbox_prj.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/inspectionProfiles/profiles_settings.xml b/YouTubeMDBot/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100755
index 0000000..105ce2d
--- /dev/null
+++ b/YouTubeMDBot/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/misc.xml b/YouTubeMDBot/.idea/misc.xml
new file mode 100755
index 0000000..8656114
--- /dev/null
+++ b/YouTubeMDBot/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/modules.xml b/YouTubeMDBot/.idea/modules.xml
new file mode 100755
index 0000000..48cc268
--- /dev/null
+++ b/YouTubeMDBot/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/.idea/vcs.xml b/YouTubeMDBot/.idea/vcs.xml
new file mode 100755
index 0000000..6c0b863
--- /dev/null
+++ b/YouTubeMDBot/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YouTubeMDBot/__init__.py b/YouTubeMDBot/__init__.py
new file mode 100755
index 0000000..986bc7c
--- /dev/null
+++ b/YouTubeMDBot/__init__.py
@@ -0,0 +1,43 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from .api import YouTubeAPI
+from .api import YouTubeVideoData
+
+from .audio import FPCalc
+from .audio import FFmpegOGG
+from .audio import FFmpegMP3
+from .audio import FFmpegOpener
+from .audio import ffmpeg_available
+
+from .commands import StartHandler
+
+from .constants import *
+
+from .decorators import restricted
+from .decorators import send_action
+
+from .downloader import YouTubeDownloader
+
+from .errors import EmptyBodyError
+
+from .logging_utils import LoggingHandler
+from .logging_utils import setup_logging
+
+from .metadata import AudioMetadata
+from .metadata import MetadataIdentifier
+from .metadata import YouTubeMetadataIdentifier
+
+from .utils import get_yt_video_id
diff --git a/YouTubeMDBot/__main__.py b/YouTubeMDBot/__main__.py
new file mode 100755
index 0000000..8a858f1
--- /dev/null
+++ b/YouTubeMDBot/__main__.py
@@ -0,0 +1,15 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
diff --git a/YouTubeMDBot/api/__init__.py b/YouTubeMDBot/api/__init__.py
new file mode 100755
index 0000000..4ba8496
--- /dev/null
+++ b/YouTubeMDBot/api/__init__.py
@@ -0,0 +1,17 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..api.youtube_api import YouTubeAPI
+from ..api.youtube_api import YouTubeVideoData
diff --git a/YouTubeMDBot/api/youtube_api.py b/YouTubeMDBot/api/youtube_api.py
new file mode 100755
index 0000000..d8ff82f
--- /dev/null
+++ b/YouTubeMDBot/api/youtube_api.py
@@ -0,0 +1,143 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from isodate import parse_duration
+
+from ..constants import YOUTUBE
+from ..errors import EmptyBodyError
+
+
+class YouTubeVideoData:
+ """
+ Obtains YouTube video data and wraps it inside this class. All fields are direct
+ access available, so it is possible to access them directly:
+ - title
+ - id
+ - thumbnail
+ - artist
+ - duration
+ - views
+ - likes
+ - dislikes
+ """
+
+ def __init__(self, data: dict, ignore_errors: bool = False):
+ """
+ By passing a dict with the YouTube data (YouTube API v3), generate and obtain
+ the information available from the result.
+ :param data: a dictionary with the information obtained from YouTube API.
+ :param ignore_errors: whether to ignore or not errors (do not raise exceptions).
+ :raises EmptyBodyError when there is no information available and ignored
+ errors is False.
+ """
+ if not data.get("items"):
+ raise EmptyBodyError("The data object has no items")
+ self.id: str = ""
+ self.title: str = ""
+ self.thumbnail: str = ""
+ self.artist: str = ""
+ self.duration: float = 0.0
+ self.views: int = 0
+ self.likes: int = 0
+ self.dislikes: int = 0
+ if len(data.get("items")) >= 1:
+ content = data.get("items")[0]
+ snippet = content.get("snippet")
+ details = content.get("contentDetails")
+ statistics = content.get("statistics")
+ if not snippet and not ignore_errors:
+ raise EmptyBodyError("No information available to requested video")
+ elif not snippet and ignore_errors:
+ snippet_available = False
+ else:
+ snippet_available = True
+ if not details and not ignore_errors:
+ raise EmptyBodyError("No video details available")
+ elif not details and ignore_errors:
+ details_available = False
+ else:
+ details_available = True
+ if not statistics and not ignore_errors:
+ raise EmptyBodyError("No statistics available")
+ elif not statistics and ignore_errors:
+ statistics_available = False
+ else:
+ statistics_available = True
+ c_id = content.get("id", "")
+ self.id = c_id.get("videoId", "") if isinstance(c_id, dict) else c_id
+ if snippet_available:
+ self.title = snippet["title"]
+ try:
+ self.thumbnail = snippet["thumbnails"]["maxres"]["url"]
+ except KeyError:
+ try:
+ self.thumbnail = snippet["thumbnails"]["high"]["url"]
+ except KeyError:
+ try:
+ self.thumbnail = snippet["thumbnails"]["medium"]["url"]
+ except KeyError:
+ self.thumbnail = snippet["thumbnails"]["default"]["url"]
+ self.artist = snippet["channelTitle"]
+ if details_available:
+ self.duration = parse_duration(details["duration"]).total_seconds()
+ if statistics_available:
+ self.views = int(statistics["viewCount"])
+ self.likes = int(statistics["likeCount"])
+ self.dislikes = int(statistics["dislikeCount"])
+
+
+class YouTubeAPI:
+ """
+ Wrapper for the YouTube API data. Allows the developer searching for videos and,
+ with a given video ID, obtain its data.
+ """
+
+ def __init__(self):
+ from googleapiclient.discovery import build
+
+ self.__youtube = build(serviceName=YOUTUBE["api"]["name"],
+ version=YOUTUBE["api"]["version"],
+ developerKey=YOUTUBE["key"])
+
+ def search(self, term: str) -> dict:
+ """
+ Searchs for a video with the specified term.
+ :param term: the search term.
+ :return: dict with YouTube data - can be wrapped inside "YouTubeVideoData" class.
+ """
+ return self.__youtube.search().list(
+ q=term,
+ type="video",
+ part="id,snippet",
+ maxResults=1
+ ).execute()
+
+ @staticmethod
+ def video_details(video_id: str) -> YouTubeVideoData:
+ """
+ Generates a "YouTubeVideoData" object wrapper with the video ID information.
+ :param video_id: YouTube video ID.
+ :return: YouTubeVideoData object with the available metadata.
+ """
+ try:
+ import ujson as json
+ except ImportError:
+ import json
+ from urllib.request import urlopen
+
+ youtube_information = YOUTUBE.copy()
+ api_url = youtube_information["endpoint"].format(video_id, YOUTUBE["key"])
+ data = urlopen(url=api_url)
+ return YouTubeVideoData(data=json.loads(data.read()), ignore_errors=True)
diff --git a/YouTubeMDBot/audio/__init__.py b/YouTubeMDBot/audio/__init__.py
new file mode 100755
index 0000000..49eab31
--- /dev/null
+++ b/YouTubeMDBot/audio/__init__.py
@@ -0,0 +1,20 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..audio.ffmpeg import FFmpegOpener
+from ..audio.ffmpeg import FFmpegOGG
+from ..audio.ffmpeg import FFmpegMP3
+from ..audio.ffmpeg import ffmpeg_available
+from ..audio.fpcalc import FPCalc
diff --git a/YouTubeMDBot/audio/ffmpeg.py b/YouTubeMDBot/audio/ffmpeg.py
new file mode 100755
index 0000000..f94c5ae
--- /dev/null
+++ b/YouTubeMDBot/audio/ffmpeg.py
@@ -0,0 +1,171 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from abc import ABC
+from abc import abstractmethod
+from typing import List
+from subprocess import PIPE
+from subprocess import Popen
+
+from ..constants import FFMPEG_OPENER
+from ..constants import FFMPEG_CONVERTER
+
+
+def ffmpeg_available() -> bool:
+ """
+ Checks if "ffmpeg" is installed or not.
+ :return: True if installed, else False
+ """
+ try:
+ proc = Popen(["ffmpeg", "-version"],
+ stdout=PIPE,
+ stderr=PIPE)
+ except OSError:
+ return False
+ else:
+ proc.wait()
+ return proc.returncode == 0
+
+
+class FFmpeg(ABC):
+ """
+ Base abstract class for the FFmpeg operators. All classes that works with FFmpeg
+ must inherit from this class in order to maintain readability and code optimization.
+
+ Allows execution of the ffmpeg command by using the subprocess module. Everything
+ is working with PIPEs, so there is no directly discs operations (everything is
+ loaded and working with RAM).
+ """
+
+ def __init__(self, data: bytes, command: List[str] = None):
+ """
+ Creates the class by passing the data which will be processed and the command (
+ by default, None).
+ :param data: audio data that will be processed.
+ :param command: the ffmpeg command.
+ """
+ self._data = data
+ self.__command = command
+ self.__out = None
+ self.__err = None
+
+ def process(self) -> int:
+ """
+ Runs the ffmpeg command in a separate process and pipes both stdout and stderr.
+ :return: the return code of the operation ('0' if everything is OK, > 0 if not).
+ """
+ proc = Popen(self.__command, stdout=PIPE, stderr=PIPE, stdin=PIPE)
+ self.__out, self.__err = proc.communicate(self._data)
+ return proc.returncode
+
+ def get_command(self) -> List[str]:
+ """
+ Get the command for editing.
+ :return: List[str] with the command - as this is a pointer, all editions done
+ to the list are directly changing the self object.
+ """
+ return self.__command
+
+ def set_command(self, command: List[str]):
+ """
+ Sets the new list, overriding every old implementation.
+ :param command: the new command.
+ """
+ self.__command = command
+
+ def get_output(self) -> bytes:
+ """
+ Gets the stdout of the process.
+ :return: bytes with the command output.
+ """
+ return self.__out
+
+ def get_extra(self) -> bytes:
+ """
+ Gets the stderr of the process.
+ :return: bytes with extra information.
+ """
+ return self.__err
+
+
+class FFmpegOpener(FFmpeg):
+ """
+ Opens and produces and audio in PWM mode.
+ """
+
+ def __init__(self, data: bytes):
+ super().__init__(data=data, command=FFMPEG_OPENER.copy())
+
+
+class FFmpegExporter(FFmpeg):
+ """
+ Base class for the exporter options available in ffmpeg.
+ All classes that are developed for converting audio files must inherit from this
+ class and implement the "convert" method.
+ """
+
+ def __init__(self, data: bytes, bitrate: str = None):
+ """
+ Generates a new instance of the class.
+ :param data: the audio data.
+ :param bitrate: the new bitrate of the audio data, or None for keeping its
+ default.
+ """
+ super().__init__(data=data, command=FFMPEG_CONVERTER.copy())
+ self._bitrate = bitrate
+
+ @abstractmethod
+ def convert(self) -> int:
+ """
+ Converts the audio to the desired format.
+ :return: the operation result code.
+ :raises NotImplementedError when trying to access this method directly on super
+ class.
+ """
+ raise NotImplementedError
+
+
+class FFmpegMP3(FFmpegExporter):
+ """
+ Exports audio data to MP3 format.
+ """
+ def convert(self) -> int:
+ command = super().get_command()
+ if self._bitrate:
+ command.append("-b:a")
+ command.append(self._bitrate)
+ command.append("-acodec")
+ command.append("libmp3lame")
+ command.append("-f")
+ command.append("mp3")
+ command.append("-")
+ return self.process()
+
+
+class FFmpegOGG(FFmpegExporter):
+ """
+ Exports audio data to OGG format.
+ """
+ def convert(self) -> int:
+ command = super().get_command()
+ if self._bitrate:
+ command.append("-b:a")
+ command.append(self._bitrate)
+ command.append("-c:a")
+ command.append("libvorbis")
+ command.append("-f")
+ command.append("ogg")
+ command.append("-")
+ return self.process()
diff --git a/YouTubeMDBot/audio/fpcalc.py b/YouTubeMDBot/audio/fpcalc.py
new file mode 100755
index 0000000..5629b12
--- /dev/null
+++ b/YouTubeMDBot/audio/fpcalc.py
@@ -0,0 +1,70 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import re
+from subprocess import PIPE
+from subprocess import Popen
+
+from ..constants import FPCALC
+
+
+def is_fpcalc_available() -> bool:
+ """
+ Checks if ffmpeg is installed in the system.
+ :return: True if available, else False.
+ """
+ try:
+ proc = Popen(["fpcalc", "-v"], stdout=PIPE, stderr=PIPE)
+ except OSError:
+ return False
+ else:
+ proc.wait()
+
+
+class FPCalc:
+ """
+ Calculates audio fingerprint by passing the audio bytes.
+ It operates with pipes so no file is created.
+ """
+ def __init__(self, audio: bytes):
+ """
+ Creates the FPCalc object.
+ :param audio: the audio bytes.
+ """
+ fpcalc = Popen(FPCALC, stdout=PIPE, stdin=PIPE)
+ out, _ = fpcalc.communicate(audio)
+ res = out.decode("utf-8")
+
+ duration_pattern = "[^=]\\d+\\n"
+ fingerprint_pattern = "[^=]*$"
+ duration = re.search(duration_pattern, res)
+ fingerprint = re.search(fingerprint_pattern, res)
+
+ self.__duration: int = int(duration.group(0))
+ self.__fp: str = str(fingerprint.group(0))
+
+ def duration(self) -> int:
+ """
+ Obtains the audio duration in seconds.
+ :return: duration in seconds.
+ """
+ return self.__duration
+
+ def fingerprint(self) -> str:
+ """
+ Obtains the audio fingerprint.
+ :return: fingerprint in seconds.
+ """
+ return self.__fp
diff --git a/YouTubeMDBot/bot.py b/YouTubeMDBot/bot.py
new file mode 100755
index 0000000..10b68d9
--- /dev/null
+++ b/YouTubeMDBot/bot.py
@@ -0,0 +1,37 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from telegram.ext import Updater
+from telegram.ext import CommandHandler
+
+PROGRAM_ARGS = None
+
+
+def main(args: dict):
+ global PROGRAM_ARGS
+ PROGRAM_ARGS = args
+ updater = Updater(args["token"], workers=args["workers"])
+
+ dispatcher = updater.dispatcher
+ dispatcher.add_handler(CommandHandler("hello", main))
+
+ if args["use_webhook"]:
+ updater.start_webhook(listen=args["ip"],
+ port=args["port"],
+ url_path=args["token"],
+ webhook_url=args["public_url"] + '/' + args["token"])
+ else:
+ updater.start_polling(args["poll_interval"])
+ updater.idle()
diff --git a/YouTubeMDBot/commands/StartHandler.py b/YouTubeMDBot/commands/StartHandler.py
new file mode 100755
index 0000000..8d18c8e
--- /dev/null
+++ b/YouTubeMDBot/commands/StartHandler.py
@@ -0,0 +1,24 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+class StartHandler(object):
+ def __init__(self):
+ self._user_data = {}
+
+ def start(self, bot, update):
+ pass
+ # TODO
diff --git a/YouTubeMDBot/commands/__init__.py b/YouTubeMDBot/commands/__init__.py
new file mode 100755
index 0000000..8a858f1
--- /dev/null
+++ b/YouTubeMDBot/commands/__init__.py
@@ -0,0 +1,15 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
diff --git a/YouTubeMDBot/constants/__init__.py b/YouTubeMDBot/constants/__init__.py
new file mode 100755
index 0000000..12af4b1
--- /dev/null
+++ b/YouTubeMDBot/constants/__init__.py
@@ -0,0 +1,22 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..constants.app_constants import ACOUSTID_KEY
+from ..constants.app_constants import FPCALC
+from ..constants.app_constants import YDL_CLI_OPTIONS
+from ..constants.app_constants import YOUTUBE
+from ..constants.app_constants import PROGRAM_ARGS
+from ..constants.app_constants import FFMPEG_OPENER
+from ..constants.app_constants import FFMPEG_CONVERTER
diff --git a/YouTubeMDBot/constants/app_constants.py b/YouTubeMDBot/constants/app_constants.py
new file mode 100755
index 0000000..1ef1e70
--- /dev/null
+++ b/YouTubeMDBot/constants/app_constants.py
@@ -0,0 +1,43 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import os
+import sys
+
+PROGRAM_ARGS = sys.argv
+# YouTube DL options
+YDL_CLI_OPTIONS = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", "--output",
+ "-"]
+
+# FPCalc command
+FPCALC = ["fpcalc", "-"]
+
+# API keys
+ACOUSTID_KEY = os.environ["ACOUSTID_KEY"]
+YOUTUBE = {
+ "key": os.environ["YOUTUBE_KEY"],
+ "api": {
+ "name": "youtube",
+ "version": "v3"
+ },
+ "endpoint":
+ "https://www.googleapis.com/youtube/v3/videos?"
+ "part=id,snippet,contentDetails,statistics&id={0}&key={1}"
+}
+
+# FFmpeg commands
+FFMPEG_OPENER = "ffmpeg -i - -f s16le -".split(" ")
+FFMPEG_CONVERTER = ["ffmpeg", "-i", "-", "-vn", "-map_metadata", "0",
+ "-movflags", "use_metadata_tags"]
diff --git a/YouTubeMDBot/decorators/__init__.py b/YouTubeMDBot/decorators/__init__.py
new file mode 100755
index 0000000..9a87cd2
--- /dev/null
+++ b/YouTubeMDBot/decorators/__init__.py
@@ -0,0 +1,17 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..decorators.decorators import restricted
+from ..decorators.decorators import send_action
diff --git a/YouTubeMDBot/decorators/decorators.py b/YouTubeMDBot/decorators/decorators.py
new file mode 100755
index 0000000..b3463c4
--- /dev/null
+++ b/YouTubeMDBot/decorators/decorators.py
@@ -0,0 +1,50 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from functools import wraps
+
+from ..constants import PROGRAM_ARGS
+
+
+def send_action(action):
+ """
+ Sends an action while processing a command.
+ :param action: the action to be performed.
+ :return: itself.
+ """
+
+ def decorator(func):
+ @wraps(func)
+ def command_func(update, context, *args, **kwargs):
+ context.bot.send_chat_action(chat_id=update.effective_message.chat_id, action=action)
+ return func(update, context, *args, **kwargs)
+ return command_func
+
+ return decorator
+
+
+def restricted(func):
+ """
+ Restricts a specific function to be accessed from non-authorized users.
+ :param func: function to be wrapped.
+ :return: itself.
+ """
+ @wraps(func)
+ def wrapped(update, context, *args, **kwargs):
+ user_id = update.effective_user.id
+ if user_id not in PROGRAM_ARGS["admin"]:
+ return
+ return func(update, context, *args, **kwargs)
+ return wrapped
diff --git a/YouTubeMDBot/downloader/__init__.py b/YouTubeMDBot/downloader/__init__.py
new file mode 100755
index 0000000..89c3cee
--- /dev/null
+++ b/YouTubeMDBot/downloader/__init__.py
@@ -0,0 +1,16 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..downloader.youtube_downloader import YouTubeDownloader
diff --git a/YouTubeMDBot/downloader/youtube_downloader.py b/YouTubeMDBot/downloader/youtube_downloader.py
new file mode 100755
index 0000000..69c8edb
--- /dev/null
+++ b/YouTubeMDBot/downloader/youtube_downloader.py
@@ -0,0 +1,58 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from io import BytesIO
+from typing import Tuple
+
+from ..constants.app_constants import YDL_CLI_OPTIONS
+
+
+class YouTubeDownloader:
+ """
+ Download a YouTube video directly into memory.
+ """
+ def __init__(self, url: str):
+ """
+ Creates the YouTubeDownloader object. Call "download" for obtaining the video.
+ :param url: the video URL.
+ """
+ self.__url: str = url
+ self.__options: list = YDL_CLI_OPTIONS.copy()
+ self.__options.append(self.__url)
+
+ def download(self) -> Tuple[BytesIO, bytes]:
+ """
+ Downloads the YouTube video directly into memory by using pipes.
+ :return: a tuple with "BytesIO" and "bytes".
+ """
+ import subprocess
+
+ proc = subprocess.Popen(self.__options,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = proc.communicate()
+ retcode = proc.returncode
+ if retcode == 0:
+ return BytesIO(stdout), stdout
+ else:
+ raise RuntimeError("youtube-dl downloader exception - more info: " +
+ str(stderr.decode("utf-8")))
+
+ def get_url(self) -> str:
+ """
+ Obtains the video URL.
+ :return: str with the URL.
+ """
+ return self.__url
diff --git a/YouTubeMDBot/errors/EmptyBodyError.py b/YouTubeMDBot/errors/EmptyBodyError.py
new file mode 100755
index 0000000..a2e8f81
--- /dev/null
+++ b/YouTubeMDBot/errors/EmptyBodyError.py
@@ -0,0 +1,23 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+class EmptyBodyError(Exception):
+ """
+ Raises an exception when the body of the json data is empty (e.g.: there is no
+ video information)
+ """
+ pass
diff --git a/YouTubeMDBot/errors/__init__.py b/YouTubeMDBot/errors/__init__.py
new file mode 100755
index 0000000..98ce0a2
--- /dev/null
+++ b/YouTubeMDBot/errors/__init__.py
@@ -0,0 +1,16 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..errors.EmptyBodyError import EmptyBodyError
diff --git a/YouTubeMDBot/logging_utils/__init__.py b/YouTubeMDBot/logging_utils/__init__.py
new file mode 100755
index 0000000..45c2549
--- /dev/null
+++ b/YouTubeMDBot/logging_utils/__init__.py
@@ -0,0 +1,17 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..logging_utils.utils import LoggingHandler
+from ..logging_utils.utils import setup_logging
diff --git a/YouTubeMDBot/logging_utils/utils.py b/YouTubeMDBot/logging_utils/utils.py
new file mode 100755
index 0000000..3b6ac76
--- /dev/null
+++ b/YouTubeMDBot/logging_utils/utils.py
@@ -0,0 +1,164 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import logging
+
+
+def cleanup_old_logs(log_file: str):
+ """
+ Cleans-up the old log files.
+ :param log_file: log filename that must be cleaned.
+ """
+ import tarfile
+ import os
+
+ tar_log_filename = log_file + ".tar.gz"
+
+ if os.path.exists(log_file):
+ if os.path.exists(tar_log_filename):
+ os.remove(tar_log_filename)
+ with tarfile.open(tar_log_filename, "w:gz") as tar:
+ tar.add(log_file, arcname=os.path.basename(log_file))
+ tar.close()
+ os.remove(log_file)
+
+
+def setup_logging(logger_name: str, log_file: str, level=logging.DEBUG,
+ formatter: str = "%(process)d - %(asctime)s | [%(levelname)s]: %(message)s"):
+ """
+ Creates a new logging which can log to stdout or file.
+ :param logger_name: the logger name.
+ :param log_file: log filename.
+ :param level: logging level.
+ :param formatter: the logging formatter.
+ :return: the logging file handler.
+ """
+ from os import path
+ from os import makedirs
+
+ log_dir = path.dirname(path.abspath(log_file))
+ if not path.exists(log_dir):
+ makedirs(log_dir)
+
+ cleanup_old_logs(log_file)
+ new_logging = logging.getLogger(logger_name)
+ logging_formatter = logging.Formatter(formatter)
+ logging_file_handler = logging.FileHandler(log_file, mode="w")
+
+ logging_file_handler.setFormatter(logging_formatter)
+
+ new_logging.setLevel(level)
+ new_logging.addHandler(logging_file_handler)
+
+ return logging_file_handler
+
+
+class LoggingHandler:
+ """
+ LoggingHandler singleton class that outputs to multiple logging instances.
+ """
+
+ class __LoggingHandler:
+ def __init__(self, logs: list):
+ self.__logs = logs
+
+ def debug(self, msg):
+ for log in self.__logs:
+ log.debug(msg)
+
+ def info(self, msg):
+ for log in self.__logs:
+ log.info(msg)
+
+ def error(self, msg):
+ for log in self.__logs:
+ log.error(msg)
+
+ def warning(self, msg):
+ for log in self.__logs:
+ log.warning(msg)
+
+ def critical(self, msg):
+ for log in self.__logs:
+ log.critical(msg)
+
+ def get_loggers(self) -> list:
+ return self.__logs
+
+ __instance = None
+
+ def __new__(cls, *args, **kwargs):
+ """
+ Generates a new instance.
+ :param args: not used.
+ :param kwargs: "logs" is a list instance that must be provided the first time
+ this class is created.
+ :return: the LoggingHandler instance.
+ """
+ if not LoggingHandler.__instance:
+ logs = kwargs.get("logs")
+ if not logs or len(logs) == 0:
+ raise AttributeError(
+ "At least kwarg \"log\" (a list of the loggers) must be provided")
+ LoggingHandler.__instance = LoggingHandler.__LoggingHandler(logs)
+ return LoggingHandler.__instance
+
+ def __getattr__(self, item):
+ return getattr(self.__instance, item)
+
+ def __setattr__(self, key, value):
+ return setattr(self.__instance, key, value)
+
+ def debug(self, msg):
+ """
+ Debugs to loggers
+ :param msg: message to debug
+ """
+ self.__instance.debug(msg)
+
+ def info(self, msg):
+ """
+ Info to loggers
+ :param msg: message to info
+ """
+ self.__instance.info(msg)
+
+ def error(self, msg):
+ """
+ Error to loggers
+ :param msg: message to error
+ """
+ self.__instance.error(msg)
+
+ def warning(self, msg):
+ """
+ Warns to loggers
+ :param msg: message to warn
+ """
+ self.__instance.warning(msg)
+
+ def critical(self, msg):
+ """
+ Critical to loggers
+ :param msg: message to critical
+ """
+ self.__instance.critical(msg)
+
+ def get_loggers(self) -> list:
+ """
+ Obtains the list of loggers.
+ :return: the list of loggers.
+ """
+ return self.__instance.get_loggers()
diff --git a/YouTubeMDBot/metadata/AudioMetadata.py b/YouTubeMDBot/metadata/AudioMetadata.py
new file mode 100755
index 0000000..77754e1
--- /dev/null
+++ b/YouTubeMDBot/metadata/AudioMetadata.py
@@ -0,0 +1,78 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from mutagen.mp4 import MP4
+from mutagen.mp4 import MP4Cover
+from io import BytesIO
+
+
+class AudioMetadata:
+ """
+ Wrapper class for setting the audio metadata to the downloaded YouTube video
+ object. By using this class, it is possible to set the required information by
+ using mutagen without having to remember the metadata keys.
+ """
+ def __init__(self, audio: BytesIO):
+ """
+ Generates a new instance.
+ :param audio: the audio metadata, in BytesIO, in MP4 format.
+ """
+ self._audio = MP4(audio)
+ self._data = audio
+
+ def set_title(self, title: str):
+ """
+ Sets the audio title.
+ :param title: the audio title.
+ """
+ self._audio[u"\xa9nam"] = title
+
+ def set_artist(self, artist: str):
+ """
+ Sets the audio artist.
+ :param artist: the audio artist.
+ """
+ self._audio[u"\xa9ART"] = artist
+
+ def set_album(self, album: str):
+ """
+ Sets the audio album.
+ :param album: the audio album
+ """
+ self._audio[u"\xa9alb"] = album
+
+ def set_extras(self, extras: list):
+ """
+ Sets the audio extras.
+ :param extras: a list of extras that will be added to the audio information.
+ """
+ self._audio[u"\xa9cmt"] = '; '.join(map(str, extras))
+
+ def set_cover(self, cover: bytes):
+ """
+ Sets the audio cover.
+ :param cover: the audio cover.
+ """
+ mp4_cover = MP4Cover(cover, MP4Cover.FORMAT_JPEG)
+ self._audio[u"covr"] = [mp4_cover]
+
+ def save(self) -> BytesIO:
+ """
+ Saves the new metadata into the audio file object.
+ :return: the audio file object with the new metadata.
+ """
+ self._data.seek(0)
+ self._audio.save(self._data)
+ return self._data
diff --git a/YouTubeMDBot/metadata/MetadataIdentifier.py b/YouTubeMDBot/metadata/MetadataIdentifier.py
new file mode 100755
index 0000000..d3396aa
--- /dev/null
+++ b/YouTubeMDBot/metadata/MetadataIdentifier.py
@@ -0,0 +1,175 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+try:
+ import ujson as json
+except ImportError:
+ import json
+import acoustid
+import musicbrainzngs
+
+from ..audio import FPCalc
+from ..api import YouTubeAPI
+from ..utils import youtube_utils
+from ..constants import ACOUSTID_KEY
+from ..downloader import YouTubeDownloader
+
+
+class MetadataIdentifier:
+ """
+ Base identifier class. By using the audio data, calculates and generates a
+ fingerprint for searching across the MusicBrainz database.
+
+ Once the audio has been identified, the available params and information are:
+ - audio (bytes)
+ - result (json)
+ - artist (str)
+ - title (str)
+ - release_id (str)
+ - recording_id (str)
+ - score (float)
+ - cover (bytes)
+ - album (str)
+ - duration (int)
+ - youtube_data (bool)
+ - youtube_id (str)
+ """
+ def __init__(self, audio: bytes):
+ """
+ Generates a new instance of the MetadataIdentifier class.
+ :param audio: the audio data, in bytes.
+ """
+ self.audio = audio
+ self.result: json = None
+ self.artist: str = ""
+ self.title: str = ""
+ self.release_id: str = ""
+ self.recording_id: str = ""
+ self.score: float = 0.0
+ self.cover: bytes = bytes(0)
+ self.album: str = ""
+ self.duration: int = 0
+ self.youtube_data: bool = False
+ self.youtube_id: str = ""
+
+ @staticmethod
+ def _is_valid_result(data: json) -> bool:
+ """
+ Checks whether the obtained result, in json, is valid or not, by checking for
+ certain keys that must exist.
+ :param data: the result in json.
+ :return: 'True' if the result is valid, else 'False'.
+ """
+ if "results" not in data:
+ return False
+ elif data["status"] != "ok":
+ return False
+ elif len(data["results"]) == 0:
+ return False
+ else:
+ if "recordings" not in data["results"][0]:
+ return False
+ else:
+ return True
+
+ def identify_audio(self) -> bool:
+ """
+ Tries to identify the audio by using the audio fingerprint. If the audio has
+ been successfully identified, then obtains all the data related to it.
+ :return: 'True' if the result is valid (the audio was correctly identified),
+ else 'False'.
+ """
+ fingerprint = FPCalc(self.audio)
+ data: json = acoustid.lookup(apikey=ACOUSTID_KEY,
+ fingerprint=fingerprint.fingerprint(),
+ duration=fingerprint.duration(),
+ meta="recordings releaseids releasegroups")
+ self.result = data
+ is_valid = self._is_valid_result(data)
+ if is_valid:
+ for result in data["results"]:
+ if "recordings" not in result:
+ break
+ self.score = result["score"]
+ for recording in result["recordings"]:
+ if recording.get("artists"):
+ names = [artist["name"] for artist in recording["artists"]]
+ self.artist = "; ".join(names)
+ else:
+ self.artist = "Unknown"
+ self.title = recording["title"]
+ if recording.get("releasegroups"):
+ self.release_id = \
+ recording["releasegroups"][0]["releases"][0]["id"]
+ self.album = recording["releasegroups"][0]["title"]
+ self.cover = musicbrainzngs.get_image_front(self.release_id)
+ self.recording_id = recording["id"]
+ self.duration = recording["duration"]
+ is_valid = True
+ break
+ break
+ return is_valid
+
+
+class YouTubeMetadataIdentifier(MetadataIdentifier):
+ """
+ Identifies YouTube metadata by using MusicBrainz database and YouTube metadata. If
+ the first identification (MusicBrainz) fails, then fallback to YouTube
+ identification if the "downloader" was provided.
+
+ Once the audio has been identified, the available params and information are:
+ - audio (bytes)
+ - result (json)
+ - artist (str)
+ - title (str)
+ - release_id (str)
+ - recording_id (str)
+ - score (float)
+ - cover (bytes)
+ - album (str)
+ - duration (int)
+ - youtube_data (bool)
+ - youtube_id (str)
+
+ If "youtube_data" is True, then only audio, title, artist, duration, cover and
+ youtube_id are available.
+ """
+ def __init__(self, audio: bytes, downloader: YouTubeDownloader = None):
+ """
+ Generates a new instance of the MetadataIdentifier class.
+ :param audio: the audio data, in bytes.
+ :param downloader: a downloader object, for obtaining the video information if
+ MusicBrainz fails.
+ """
+ super().__init__(audio)
+ self._downloader = downloader
+
+ def identify_audio(self) -> bool:
+ valid = super().identify_audio()
+ if not valid:
+ if self._downloader:
+ from urllib.request import urlopen
+
+ video_id = youtube_utils.get_yt_video_id(self._downloader.get_url())
+ video_data = YouTubeAPI.video_details(video_id)
+ self.title = video_data.title
+ self.artist = video_data.artist
+ self.duration = video_data.duration
+ self.cover = urlopen(video_data.thumbnail).read()
+ self.youtube_id = video_data.id
+ self.youtube_data = True
+
+ valid = True
+ return valid
diff --git a/YouTubeMDBot/metadata/__init__.py b/YouTubeMDBot/metadata/__init__.py
new file mode 100755
index 0000000..4abadb9
--- /dev/null
+++ b/YouTubeMDBot/metadata/__init__.py
@@ -0,0 +1,19 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..metadata.MetadataIdentifier import MetadataIdentifier
+from ..metadata.MetadataIdentifier import YouTubeMetadataIdentifier
+
+from ..metadata.AudioMetadata import AudioMetadata
diff --git a/YouTubeMDBot/requirements.txt b/YouTubeMDBot/requirements.txt
new file mode 100755
index 0000000..e341c34
--- /dev/null
+++ b/YouTubeMDBot/requirements.txt
@@ -0,0 +1,8 @@
+mutagen
+isodate
+google-api-python-client
+musicbrainzngs
+ujson
+youtube_dl
+pyacoustid
+python-telegram-bot
diff --git a/YouTubeMDBot/tests/converter.py b/YouTubeMDBot/tests/converter.py
new file mode 100644
index 0000000..17aac27
--- /dev/null
+++ b/YouTubeMDBot/tests/converter.py
@@ -0,0 +1,36 @@
+import unittest
+import mutagen
+
+from typing import Tuple
+from io import BytesIO
+
+from YouTubeMDBot.tests.tagger import TaggerTest
+from YouTubeMDBot.downloader import YouTubeDownloader
+from YouTubeMDBot.audio import FFmpegMP3
+from YouTubeMDBot.audio import FFmpegOGG
+
+
+class MyTestCase(TaggerTest):
+ def find_metadata(self, downloader: YouTubeDownloader) -> Tuple[BytesIO, bytes]:
+ io, data = super().find_metadata(downloader)
+ io.seek(0)
+ mp3 = FFmpegMP3(data=data, bitrate="96k") # downrate
+ ogg = FFmpegOGG(data=data, bitrate="256k") # uprate
+
+ mp3.convert()
+ ogg.convert()
+
+ mp3_container = BytesIO(mp3.get_output())
+ ogg_container = BytesIO(ogg.get_output())
+
+ print(mp3.get_extra().decode("utf-8"))
+ print(ogg.get_extra().decode("utf-8"))
+
+ print(mutagen.File(mp3_container).pprint())
+ print(mutagen.File(ogg_container).pprint())
+
+ return io, data
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/YouTubeMDBot/tests/downloader.py b/YouTubeMDBot/tests/downloader.py
new file mode 100755
index 0000000..cb53f74
--- /dev/null
+++ b/YouTubeMDBot/tests/downloader.py
@@ -0,0 +1,46 @@
+import threading
+import unittest
+from time import sleep
+
+from YouTubeMDBot.downloader import YouTubeDownloader
+
+
+class DownloadTest(unittest.TestCase):
+ _elements = 0
+ _max = 0
+ _lock = threading.Lock()
+
+ def test_multithread_download(self):
+ yt1 = YouTubeDownloader(url="https://www.youtube.com/watch?v=Inm-N5rLUSI")
+ yt2 = YouTubeDownloader(url="https://www.youtube.com/watch?v=-_ZwpOdXXcA")
+ yt3 = YouTubeDownloader(url="https://www.youtube.com/watch?v=WOGWZD5iT10")
+ yt4 = YouTubeDownloader(url="https://www.youtube.com/watch?v=9HfoNUjw5u8")
+ t1 = threading.Thread(target=self.write_to_file, args=(yt1, "v1.m4a",))
+ t2 = threading.Thread(target=self.write_to_file, args=(yt2, "v2.m4a",))
+ t3 = threading.Thread(target=self.write_to_file, args=(yt3, "v3.m4a",))
+ t4 = threading.Thread(target=self.write_to_file, args=(yt4, "v4.m4a",))
+
+ self._max = 4
+
+ t1.start()
+ t2.start()
+ t3.start()
+ t4.start()
+
+ while self._elements < self._max:
+ sleep(1)
+
+ def barrier(self):
+ with self._lock:
+ self._elements += 1
+
+ def write_to_file(self, yt: YouTubeDownloader, name: str):
+ _, data = yt.download()
+ print(name + " downloaded")
+ with open(name, "wb") as f:
+ f.write(data)
+ self.barrier()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/YouTubeMDBot/tests/identifier.py b/YouTubeMDBot/tests/identifier.py
new file mode 100755
index 0000000..747f4a0
--- /dev/null
+++ b/YouTubeMDBot/tests/identifier.py
@@ -0,0 +1,106 @@
+import threading
+import unittest
+from pprint import pprint
+from time import sleep
+from time import time
+from typing import Tuple
+from io import BytesIO
+
+from YouTubeMDBot.downloader import YouTubeDownloader
+from YouTubeMDBot.metadata import YouTubeMetadataIdentifier
+
+
+class IdentifierTest(unittest.TestCase):
+ lock = threading.Lock()
+ threads = 0
+ max = 0
+ song_info = {}
+
+ def test_identification(self):
+ url = "https://www.youtube.com/watch?v=YQHsXMglC9A"
+ downloader = YouTubeDownloader(url=url)
+ audio, data = downloader.download()
+ with open("hello.m4a", "wb") as song:
+ song.write(data)
+ identifier = YouTubeMetadataIdentifier(audio=data, downloader=downloader)
+
+ valid = identifier.identify_audio()
+ assert valid
+ print("{0} by {1} - score: {2} / 1\n"
+ "\thttps://musicbrainz.org/recording/{3}\n"
+ "\thttps://musicbrainz.org/release/{4}\n\n"
+ .format(identifier.title, identifier.artist,
+ identifier.score,
+ identifier.recording_id, identifier.release_id))
+ with open("cover.jpg", "wb") as cover:
+ cover.write(identifier.cover)
+
+ def test_multiple_download_identification(self):
+ yt1 = YouTubeDownloader(url="https://www.youtube.com/watch?v=Inm-N5rLUSI")
+ yt2 = YouTubeDownloader(url="https://www.youtube.com/watch?v=-_ZwpOdXXcA")
+ yt3 = YouTubeDownloader(url="https://www.youtube.com/watch?v=WOGWZD5iT10")
+ yt4 = YouTubeDownloader(url="https://www.youtube.com/watch?v=GfKV9KaNJXc")
+ yt5 = YouTubeDownloader(url="https://www.youtube.com/watch?v=DiItGE3eAyQ")
+ yt6 = YouTubeDownloader(url="https://www.youtube.com/watch?v=GuZzuQvv7uc")
+
+ t1 = threading.Thread(target=self.find_metadata, args=(yt1,))
+ t2 = threading.Thread(target=self.find_metadata, args=(yt2,))
+ t3 = threading.Thread(target=self.find_metadata, args=(yt3,))
+ t4 = threading.Thread(target=self.find_metadata, args=(yt4,))
+ t5 = threading.Thread(target=self.find_metadata, args=(yt5,))
+ t6 = threading.Thread(target=self.find_metadata, args=(yt6,))
+
+ self.max = 6
+
+ t1.start()
+ t2.start()
+ t3.start()
+ t4.start()
+ t5.start()
+ t6.start()
+
+ while self.threads < self.max:
+ sleep(1)
+
+ # pprint(self.song_info)
+ pprint("Finished")
+
+ def barrier(self):
+ with self.lock:
+ self.threads += 1
+
+ def getThreads(self):
+ with self.lock:
+ return self.threads
+
+ def find_metadata(self, downloader: YouTubeDownloader) -> Tuple[BytesIO, bytes]:
+ st_dl_t = time()
+ io, data = downloader.download()
+ f_dl_t = time()
+ print("Downloaded {} - elapsed time: {:.1f}s".format(downloader.get_url(),
+ f_dl_t - st_dl_t))
+ identifier = YouTubeMetadataIdentifier(audio=data, downloader=downloader)
+ valid = identifier.identify_audio()
+ assert valid
+ self.song_info[downloader.get_url()] = {
+ "title": identifier.title,
+ "artist": identifier.artist,
+ "cover": identifier.cover
+ }
+ if not identifier.youtube_data:
+ self.song_info[downloader.get_url()]["score"] = identifier.score
+ self.song_info[downloader.get_url()]["record_id"] = \
+ "https://musicbrainz.org/recording/{0}".format(identifier.recording_id)
+ self.song_info[downloader.get_url()]["release_id"] = \
+ "https://musicbrainz.org/release/{0}".format(identifier.release_id)
+ self.song_info[downloader.get_url()]["album"] = identifier.album
+ else:
+ self.song_info[downloader.get_url()]["duration"] = identifier.duration
+ self.song_info[downloader.get_url()]["id"] = identifier.youtube_id
+ self.song_info[downloader.get_url()]["youtube_data"] = True
+ self.barrier()
+ return io, data
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/YouTubeMDBot/tests/song_search.py b/YouTubeMDBot/tests/song_search.py
new file mode 100755
index 0000000..c4afadb
--- /dev/null
+++ b/YouTubeMDBot/tests/song_search.py
@@ -0,0 +1,25 @@
+import unittest
+
+from YouTubeMDBot.api import YouTubeAPI
+from YouTubeMDBot.api import YouTubeVideoData
+
+
+class TestSearch(unittest.TestCase):
+ def test_search(self):
+ s = YouTubeAPI()
+ search: dict = s.search(term="test")
+ data = YouTubeVideoData(data=search, ignore_errors=True)
+ print("Title: {0}\n"
+ "Artist: {1}\n"
+ "Thumbnail: {2}\n"
+ "Duration: {3}\n"
+ "Views: {4}\n"
+ "Likes: {5}\n"
+ "Dislikes: {6}\n"
+ "Id: {7}".format(data.title, data.artist, data.thumbnail,
+ data.duration, data.views, data.likes,
+ data.dislikes, data.id))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/YouTubeMDBot/tests/tagger.py b/YouTubeMDBot/tests/tagger.py
new file mode 100755
index 0000000..7d8ed02
--- /dev/null
+++ b/YouTubeMDBot/tests/tagger.py
@@ -0,0 +1,44 @@
+import unittest
+import mutagen
+
+from typing import Tuple
+from io import BytesIO
+
+from YouTubeMDBot.tests.identifier import IdentifierTest
+from YouTubeMDBot.downloader import YouTubeDownloader
+from YouTubeMDBot.metadata import AudioMetadata
+from YouTubeMDBot.utils import youtube_utils
+
+
+class TaggerTest(IdentifierTest):
+ def find_metadata(self, downloader: YouTubeDownloader) -> Tuple[BytesIO, bytes]:
+ io, data = super().find_metadata(downloader)
+ tagger = AudioMetadata(io)
+ url = downloader.get_url()
+
+ tagger.set_title(super().song_info[url]["title"])
+ tagger.set_artist(super().song_info[url]["artist"])
+ tagger.set_cover(super().song_info[url]["cover"])
+ extra = ["YouTube URL: " + url]
+ if not super().song_info[url].get("youtube_data"):
+ tagger.set_album(super().song_info[url]["album"])
+ extra.append("MusicBrainz Record ID: " + super().song_info[url][
+ "record_id"])
+ extra.append("MusicBrainz Release ID: " + super().song_info[url][
+ "release_id"])
+ tagger.set_extras(extra)
+ else:
+ tagger.set_extras(["YouTube ID: {}".format(super().song_info[url]["id"])])
+ yid = youtube_utils.get_yt_video_id(url)
+ rs = tagger.save()
+ rs.seek(0)
+ print(mutagen.File(rs).pprint())
+ rs.seek(0)
+ with open(yid + ".m4a", "wb") as f:
+ f.write(rs.read())
+ rs.seek(0)
+ return rs, rs.read()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/YouTubeMDBot/utils/__init__.py b/YouTubeMDBot/utils/__init__.py
new file mode 100755
index 0000000..9fe1f3e
--- /dev/null
+++ b/YouTubeMDBot/utils/__init__.py
@@ -0,0 +1,16 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from ..utils.youtube_utils import get_yt_video_id
diff --git a/YouTubeMDBot/utils/youtube_utils.py b/YouTubeMDBot/utils/youtube_utils.py
new file mode 100755
index 0000000..7d66274
--- /dev/null
+++ b/YouTubeMDBot/utils/youtube_utils.py
@@ -0,0 +1,52 @@
+# YouTubeMDBot
+# Copyright (C) 2019 - Javinator9889
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+def get_yt_video_id(url: str) -> str:
+ # initial version: http://stackoverflow.com/a/7936523/617185 \
+ # by Mikhail Kashkin(http://stackoverflow.com/users/85739/mikhail-kashkin)
+ """Returns Video_ID extracting from the given url of Youtube
+
+ Examples of URLs:
+ Valid:
+ 'http://youtu.be/_lOT2p_FCvA',
+ 'www.youtube.com/watch?v=_lOT2p_FCvA&feature=feedu',
+ 'http://www.youtube.com/embed/_lOT2p_FCvA',
+ 'http://www.youtube.com/v/_lOT2p_FCvA?version=3&hl=en_US',
+ 'https://www.youtube.com/watch?v=rTHlyTphWP0&index=6&list=PLjeDyYvG6-40qawYNR4juzvSOg-ezZ2a6',
+ 'youtube.com/watch?v=_lOT2p_FCvA',
+
+ Invalid:
+ 'youtu.be/watch?v=_lOT2p_FCvA',
+ """
+
+ from urllib.parse import urlparse
+ from urllib.parse import parse_qs
+
+ if url.startswith(('youtu', 'www')):
+ url = 'http://' + url
+
+ query = urlparse(url)
+
+ if 'youtube' in query.hostname:
+ if query.path == '/watch':
+ return parse_qs(query.query)['v'][0]
+ elif query.path.startswith(('/embed/', '/v/')):
+ return query.path.split('/')[2]
+ elif 'youtu.be' in query.hostname:
+ return query.path[1:]
+ else:
+ raise ValueError