diff options
| author | Tolmachev Igor <me@igorek.dev> | 2024-09-15 13:14:54 +0300 |
|---|---|---|
| committer | Tolmachev Igor <me@igorek.dev> | 2024-09-15 13:14:54 +0300 |
| commit | 19606a6ac95e83a33c1173dba3e5d6f9f8fb9f94 (patch) | |
| tree | 206a68ad48ba728c46bf84137106f48f5d7d4595 | |
| parent | 551e69c5ebd7a227478105ac1cc91876f1c2f601 (diff) | |
| download | video2story-19606a6ac95e83a33c1173dba3e5d6f9f8fb9f94.tar.gz video2story-19606a6ac95e83a33c1173dba3e5d6f9f8fb9f94.zip | |
Add README and fix package props
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | pyproject.toml | 7 | ||||
| -rw-r--r-- | video2story/__init__.py | 194 | ||||
| -rw-r--r-- | video2story/__main__.py | 195 | ||||
| -rw-r--r-- | video2story/cutter.py | 10 | ||||
| -rw-r--r-- | video2story/uploader.py | 2 |
6 files changed, 245 insertions, 200 deletions
| @@ -0,0 +1,37 @@ | |||
| 1 | # video2story | ||
| 2 | |||
| 3 | **video2story** is a Python tool that allows you to convert and upload videos to your Telegram stories. It automatically cuts the video into segments and uploads them to your Telegram account. | ||
| 4 | |||
| 5 | ## Installation | ||
| 6 | |||
| 7 | To install `video2story`, use pip: | ||
| 8 | |||
| 9 | ```bash | ||
| 10 | pip install video2story | ||
| 11 | ``` | ||
| 12 | |||
| 13 | ## Usage | ||
| 14 | |||
| 15 | ### 1. Process the video | ||
| 16 | |||
| 17 | ```bash | ||
| 18 | video2story video input.mp4 output_dir/ | ||
| 19 | ``` | ||
| 20 | |||
| 21 | First, split the video into segments. Replace `input.mp4` with the path to your video file and `output_dir/` with the directory where you want to save the processed segments: | ||
| 22 | |||
| 23 | By default, the video will be cut into 60-second segments. To change that, use the `-d` option and specify the duration in seconds: | ||
| 24 | |||
| 25 | ### 2. Upload the processed video | ||
| 26 | |||
| 27 | ```bash | ||
| 28 | video2story PHONE_NUMBER output_dir/ -p friends | ||
| 29 | ``` | ||
| 30 | |||
| 31 | Replace `PHONE_NUMBER` with your Telegram phone number and `output_dir/` with the directory containing the processed video segments. | ||
| 32 | |||
| 33 | The `-p` option allows you to choose the privacy level for your story. For more options and detailed information, use the `--help` flag. | ||
| 34 | |||
| 35 | ## License | ||
| 36 | |||
| 37 | This project is licensed under the GPL (GNU General Public License). See the [LICENSE](/LICENSE) file for more details. | ||
diff --git a/pyproject.toml b/pyproject.toml index 53270a9..5317250 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -1,9 +1,11 @@ | |||
| 1 | [tool.poetry] | 1 | [tool.poetry] |
| 2 | name = "video2story" | 2 | name = "video2story" |
| 3 | version = "1.0.0" | 3 | version = "1.0.1" |
| 4 | description = "Simple telegram story uploader" | 4 | description = "Simple telegram story uploader" |
| 5 | repository = "https://codeberg.org/igorechek06/video2story" | ||
| 5 | authors = ["Tolmachev Igor <me@igorek.dev>"] | 6 | authors = ["Tolmachev Igor <me@igorek.dev>"] |
| 6 | readme = "README.md" | 7 | readme = "README.md" |
| 8 | packages = [{ include = "video2story" }] | ||
| 7 | 9 | ||
| 8 | [tool.poetry.dependencies] | 10 | [tool.poetry.dependencies] |
| 9 | python = "^3.10" | 11 | python = "^3.10" |
| @@ -14,6 +16,9 @@ mypy = "^1.11.2" | |||
| 14 | isort = "^5.13.2" | 16 | isort = "^5.13.2" |
| 15 | black = "^24.8.0" | 17 | black = "^24.8.0" |
| 16 | 18 | ||
| 19 | [tool.poetry.scripts] | ||
| 20 | video2story = "video2story:main" | ||
| 21 | |||
| 17 | [tool.mypy] | 22 | [tool.mypy] |
| 18 | ignore_missing_imports = true | 23 | ignore_missing_imports = true |
| 19 | disallow_untyped_defs = true | 24 | disallow_untyped_defs = true |
diff --git a/video2story/__init__.py b/video2story/__init__.py index 77e4018..2fcf2a0 100644 --- a/video2story/__init__.py +++ b/video2story/__init__.py | |||
| @@ -1,2 +1,196 @@ | |||
| 1 | from argparse import ArgumentParser, RawTextHelpFormatter | ||
| 2 | from os.path import normpath | ||
| 3 | |||
| 1 | from .cutter import cut | 4 | from .cutter import cut |
| 2 | from .uploader import upload | 5 | from .uploader import upload |
| 6 | |||
| 7 | parser = ArgumentParser( | ||
| 8 | prog="video2story", | ||
| 9 | description="Simple telegram story uploader", | ||
| 10 | formatter_class=RawTextHelpFormatter, | ||
| 11 | ) | ||
| 12 | subparsers = parser.add_subparsers( | ||
| 13 | metavar="MODULE", | ||
| 14 | dest="module", | ||
| 15 | required=True, | ||
| 16 | ) | ||
| 17 | |||
| 18 | video_parser = subparsers.add_parser( | ||
| 19 | "video", | ||
| 20 | help="Video processing module", | ||
| 21 | formatter_class=RawTextHelpFormatter, | ||
| 22 | ) | ||
| 23 | |||
| 24 | video_parser.add_argument( | ||
| 25 | "filename", | ||
| 26 | type=str, | ||
| 27 | metavar="FILE", | ||
| 28 | help="Video file name", | ||
| 29 | ) | ||
| 30 | video_parser.add_argument( | ||
| 31 | "output", | ||
| 32 | type=str, | ||
| 33 | metavar="PATH", | ||
| 34 | help="Output folder for processed videos", | ||
| 35 | ) | ||
| 36 | video_parser.add_argument( | ||
| 37 | "-d", | ||
| 38 | "--duration", | ||
| 39 | type=int, | ||
| 40 | metavar="SECONDS", | ||
| 41 | default=60, | ||
| 42 | help=( | ||
| 43 | "Specifies each story duration.\n" | ||
| 44 | "Max value is 60 and min value is 1.\n" | ||
| 45 | "Default is 60." | ||
| 46 | ), | ||
| 47 | ) | ||
| 48 | video_parser.add_argument( | ||
| 49 | "--no-sound", | ||
| 50 | action="store_true", | ||
| 51 | help="Remove sound from video.", | ||
| 52 | ) | ||
| 53 | video_parser.add_argument( | ||
| 54 | "-s", | ||
| 55 | "--start", | ||
| 56 | type=int, | ||
| 57 | metavar="SECONDS", | ||
| 58 | default=None, | ||
| 59 | help="Specifies the second of the video from which processing will begin.", | ||
| 60 | ) | ||
| 61 | video_parser.add_argument( | ||
| 62 | "-e", | ||
| 63 | "--end", | ||
| 64 | type=int, | ||
| 65 | metavar="SECONDS", | ||
| 66 | default=None, | ||
| 67 | help="Specifies the second of the video from which processing will end.", | ||
| 68 | ) | ||
| 69 | |||
| 70 | story_parser = subparsers.add_parser( | ||
| 71 | "story", | ||
| 72 | help="Story publishing module", | ||
| 73 | formatter_class=RawTextHelpFormatter, | ||
| 74 | ) | ||
| 75 | |||
| 76 | story_parser.add_argument( | ||
| 77 | "phone", | ||
| 78 | type=str, | ||
| 79 | metavar="PHONE", | ||
| 80 | help="Your phone number.", | ||
| 81 | ) | ||
| 82 | story_parser.add_argument( | ||
| 83 | "input", | ||
| 84 | type=str, | ||
| 85 | metavar="PATH", | ||
| 86 | help="Input folder with processed videos", | ||
| 87 | ) | ||
| 88 | story_parser.add_argument( | ||
| 89 | "-p", | ||
| 90 | "--privacy", | ||
| 91 | type=str, | ||
| 92 | metavar="PRIVACY-MODE", | ||
| 93 | choices=["everyone", "contacts", "selected", "friends"], | ||
| 94 | default="everyone", | ||
| 95 | help=( | ||
| 96 | "Specifies who can see you story.\n" | ||
| 97 | "Accept values: `everyone`, `contacts`, `selected` and `friends`.\n" | ||
| 98 | "\n" | ||
| 99 | "If set to `everyone` or `contacts`, the --user flag excludes the user who can see your story.\n" | ||
| 100 | "If set to `selected`, the --user flag specifies the user who can see your story.\n" | ||
| 101 | "If set to `friends`, the --user flag will have no effect.\n" | ||
| 102 | ), | ||
| 103 | ) | ||
| 104 | story_parser.add_argument( | ||
| 105 | "-u", | ||
| 106 | "--users", | ||
| 107 | type=str, | ||
| 108 | metavar="USERS", | ||
| 109 | nargs="+", | ||
| 110 | help=( | ||
| 111 | "Behavior depends on privacy mode. See `privacy` description.\n" | ||
| 112 | "You can specify a username or user id." | ||
| 113 | ), | ||
| 114 | ) | ||
| 115 | story_parser.add_argument( | ||
| 116 | "-a", | ||
| 117 | "--active-period", | ||
| 118 | type=str, | ||
| 119 | metavar="PERIOD", | ||
| 120 | choices=["6h", "12h", "24h", "48h"], | ||
| 121 | default="24h", | ||
| 122 | help=( | ||
| 123 | "Period after which the story is moved to archive.\n" | ||
| 124 | "Accept values: `6h`, `12h`, `24h` and `48h`.\n" | ||
| 125 | "Default is 24h." | ||
| 126 | ), | ||
| 127 | ) | ||
| 128 | story_parser.add_argument( | ||
| 129 | "--save-to-profile", | ||
| 130 | action="store_true", | ||
| 131 | help="Keep the story accessible after expiration.", | ||
| 132 | ) | ||
| 133 | story_parser.add_argument( | ||
| 134 | "--protected-content", | ||
| 135 | action="store_true", | ||
| 136 | help="Protect story from forwarding and screenshotting.", | ||
| 137 | ) | ||
| 138 | story_parser.add_argument( | ||
| 139 | "--tdlib", | ||
| 140 | type=str, | ||
| 141 | metavar="PATH", | ||
| 142 | help="Path to tdlib library file.", | ||
| 143 | ) | ||
| 144 | story_parser.add_argument( | ||
| 145 | "-s", | ||
| 146 | "--start", | ||
| 147 | type=int, | ||
| 148 | metavar="VIDEO-ID", | ||
| 149 | default=0, | ||
| 150 | help=( | ||
| 151 | "Specifies the start point of the publication.\n" | ||
| 152 | "VIDEO-ID is the number in the name of processed file.\n" | ||
| 153 | "Default is 0." | ||
| 154 | ), | ||
| 155 | ) | ||
| 156 | story_parser.add_argument( | ||
| 157 | "-e", | ||
| 158 | "--end", | ||
| 159 | type=int, | ||
| 160 | metavar="VIDEO-ID", | ||
| 161 | default=None, | ||
| 162 | help=( | ||
| 163 | "Specifies the end point of the publication.\n" | ||
| 164 | "VIDEO-ID is the number in the name of processed file." | ||
| 165 | ), | ||
| 166 | ) | ||
| 167 | |||
| 168 | |||
| 169 | def main(): | ||
| 170 | args = parser.parse_args() | ||
| 171 | if args.module == "video": | ||
| 172 | if not (1 <= args.duration <= 60): | ||
| 173 | print("Duration must be between 1 and 60") | ||
| 174 | exit(1) | ||
| 175 | |||
| 176 | cut( | ||
| 177 | normpath(args.filename), | ||
| 178 | normpath(args.output), | ||
| 179 | args.duration, | ||
| 180 | args.no_sound, | ||
| 181 | args.start, | ||
| 182 | args.end, | ||
| 183 | ) | ||
| 184 | elif args.module == "story": | ||
| 185 | upload( | ||
| 186 | args.phone, | ||
| 187 | normpath(args.input), | ||
| 188 | args.privacy, | ||
| 189 | args.users, | ||
| 190 | args.active_period, | ||
| 191 | args.save_to_profile, | ||
| 192 | args.protected_content, | ||
| 193 | args.tdlib, | ||
| 194 | args.start, | ||
| 195 | args.end, | ||
| 196 | ) | ||
diff --git a/video2story/__main__.py b/video2story/__main__.py index 43c94b8..8273c4f 100644 --- a/video2story/__main__.py +++ b/video2story/__main__.py | |||
| @@ -1,194 +1,3 @@ | |||
| 1 | from argparse import ArgumentParser, RawTextHelpFormatter | 1 | from . import main |
| 2 | from os.path import normpath | ||
| 3 | 2 | ||
| 4 | from video2story import cut, upload | 3 | main() |
| 5 | |||
| 6 | parser = ArgumentParser( | ||
| 7 | prog="video2story", | ||
| 8 | description="Simple telegram story uploader", | ||
| 9 | formatter_class=RawTextHelpFormatter, | ||
| 10 | ) | ||
| 11 | subparsers = parser.add_subparsers( | ||
| 12 | metavar="MODULE", | ||
| 13 | dest="module", | ||
| 14 | required=True, | ||
| 15 | ) | ||
| 16 | |||
| 17 | video_parser = subparsers.add_parser( | ||
| 18 | "video", | ||
| 19 | help="Video processing module", | ||
| 20 | formatter_class=RawTextHelpFormatter, | ||
| 21 | ) | ||
| 22 | |||
| 23 | video_parser.add_argument( | ||
| 24 | "filename", | ||
| 25 | type=str, | ||
| 26 | metavar="FILE", | ||
| 27 | help="Video file name", | ||
| 28 | ) | ||
| 29 | video_parser.add_argument( | ||
| 30 | "output", | ||
| 31 | type=str, | ||
| 32 | metavar="PATH", | ||
| 33 | help="Output folder for processed videos", | ||
| 34 | ) | ||
| 35 | video_parser.add_argument( | ||
| 36 | "-d", | ||
| 37 | "--duration", | ||
| 38 | type=int, | ||
| 39 | metavar="SECONDS", | ||
| 40 | default=60, | ||
| 41 | help=( | ||
| 42 | "Specifies each story duration.\n" | ||
| 43 | "Max value is 60 and min value is 1.\n" | ||
| 44 | "Default is 60." | ||
| 45 | ), | ||
| 46 | ) | ||
| 47 | video_parser.add_argument( | ||
| 48 | "--no-sound", | ||
| 49 | action="store_true", | ||
| 50 | help="Remove sound from video.", | ||
| 51 | ) | ||
| 52 | video_parser.add_argument( | ||
| 53 | "-s", | ||
| 54 | "--start", | ||
| 55 | type=int, | ||
| 56 | metavar="SECONDS", | ||
| 57 | default=None, | ||
| 58 | help="Specifies the second of the video from which processing will begin.", | ||
| 59 | ) | ||
| 60 | video_parser.add_argument( | ||
| 61 | "-e", | ||
| 62 | "--end", | ||
| 63 | type=int, | ||
| 64 | metavar="SECONDS", | ||
| 65 | default=None, | ||
| 66 | help="Specifies the second of the video from which processing will end.", | ||
| 67 | ) | ||
| 68 | |||
| 69 | story_parser = subparsers.add_parser( | ||
| 70 | "story", | ||
| 71 | help="Story publishing module", | ||
| 72 | formatter_class=RawTextHelpFormatter, | ||
| 73 | ) | ||
| 74 | |||
| 75 | story_parser.add_argument( | ||
| 76 | "phone", | ||
| 77 | type=str, | ||
| 78 | metavar="PHONE", | ||
| 79 | help="Your phone number.", | ||
| 80 | ) | ||
| 81 | story_parser.add_argument( | ||
| 82 | "input", | ||
| 83 | type=str, | ||
| 84 | metavar="PATH", | ||
| 85 | help="Input folder with processed videos", | ||
| 86 | ) | ||
| 87 | story_parser.add_argument( | ||
| 88 | "-p", | ||
| 89 | "--privacy", | ||
| 90 | type=str, | ||
| 91 | metavar="PRIVACY-MODE", | ||
| 92 | choices=["everyone", "contacts", "selected", "friends"], | ||
| 93 | default="everyone", | ||
| 94 | help=( | ||
| 95 | "Specifies who can see you story.\n" | ||
| 96 | "Accept values: `everyone`, `contacts`, `selected` and `friends`.\n" | ||
| 97 | "\n" | ||
| 98 | "If set to `everyone` or `contacts`, the --user flag excludes the user who can see your story.\n" | ||
| 99 | "If set to `selected`, the --user flag specifies the user who can see your story.\n" | ||
| 100 | "If set to `friends`, the --user flag will have no effect.\n" | ||
| 101 | ), | ||
| 102 | ) | ||
| 103 | story_parser.add_argument( | ||
| 104 | "-u", | ||
| 105 | "--users", | ||
| 106 | type=str, | ||
| 107 | metavar="USERS", | ||
| 108 | nargs="+", | ||
| 109 | help=( | ||
| 110 | "Behavior depends on privacy mode. See `privacy` description.\n" | ||
| 111 | "You can specify a username or user id." | ||
| 112 | ), | ||
| 113 | ) | ||
| 114 | story_parser.add_argument( | ||
| 115 | "-a", | ||
| 116 | "--active-period", | ||
| 117 | type=str, | ||
| 118 | metavar="PERIOD", | ||
| 119 | choices=["6h", "12h", "24h", "48h"], | ||
| 120 | default="24h", | ||
| 121 | help=( | ||
| 122 | "Period after which the story is moved to archive.\n" | ||
| 123 | "Accept values: `6h`, `12h`, `24h` and `48h`.\n" | ||
| 124 | "Default is 24h." | ||
| 125 | ), | ||
| 126 | ) | ||
| 127 | story_parser.add_argument( | ||
| 128 | "--save-to-profile", | ||
| 129 | action="store_true", | ||
| 130 | help="Keep the story accessible after expiration.", | ||
| 131 | ) | ||
| 132 | story_parser.add_argument( | ||
| 133 | "--protected-content", | ||
| 134 | action="store_true", | ||
| 135 | help="Protect story from forwarding and screenshotting.", | ||
| 136 | ) | ||
| 137 | story_parser.add_argument( | ||
| 138 | "--tdlib", | ||
| 139 | type=str, | ||
| 140 | metavar="PATH", | ||
| 141 | help="Path to tdlib library file.", | ||
| 142 | ) | ||
| 143 | story_parser.add_argument( | ||
| 144 | "-s", | ||
| 145 | "--start", | ||
| 146 | type=int, | ||
| 147 | metavar="VIDEO-ID", | ||
| 148 | default=0, | ||
| 149 | help=( | ||
| 150 | "Specifies the start point of the publication.\n" | ||
| 151 | "VIDEO-ID is the number in the name of processed file.\n" | ||
| 152 | "Default is 0." | ||
| 153 | ), | ||
| 154 | ) | ||
| 155 | story_parser.add_argument( | ||
| 156 | "-e", | ||
| 157 | "--end", | ||
| 158 | type=int, | ||
| 159 | metavar="VIDEO-ID", | ||
| 160 | default=None, | ||
| 161 | help=( | ||
| 162 | "Specifies the end point of the publication.\n" | ||
| 163 | "VIDEO-ID is the number in the name of processed file." | ||
| 164 | ), | ||
| 165 | ) | ||
| 166 | |||
| 167 | |||
| 168 | args = parser.parse_args() | ||
| 169 | if args.module == "video": | ||
| 170 | if not (1 <= args.duration <= 60): | ||
| 171 | print("Duration must be between 1 and 60") | ||
| 172 | exit(1) | ||
| 173 | |||
| 174 | cut( | ||
| 175 | normpath(args.filename), | ||
| 176 | normpath(args.output), | ||
| 177 | args.duration, | ||
| 178 | args.no_sound, | ||
| 179 | args.start, | ||
| 180 | args.end, | ||
| 181 | ) | ||
| 182 | elif args.module == "story": | ||
| 183 | upload( | ||
| 184 | args.phone, | ||
| 185 | normpath(args.input), | ||
| 186 | args.privacy, | ||
| 187 | args.users, | ||
| 188 | args.active_period, | ||
| 189 | args.save_to_profile, | ||
| 190 | args.protected_content, | ||
| 191 | args.tdlib, | ||
| 192 | args.start, | ||
| 193 | args.end, | ||
| 194 | ) | ||
diff --git a/video2story/cutter.py b/video2story/cutter.py index 23f035e..0a83ad1 100644 --- a/video2story/cutter.py +++ b/video2story/cutter.py | |||
| @@ -5,15 +5,15 @@ from subprocess import Popen | |||
| 5 | 5 | ||
| 6 | def cut( | 6 | def cut( |
| 7 | filename: str, | 7 | filename: str, |
| 8 | output: str, | 8 | output_dir: str, |
| 9 | duration: int, | 9 | duration: int, |
| 10 | no_sound: bool, | 10 | no_sound: bool, |
| 11 | start: int | None, | 11 | start: int | None, |
| 12 | end: int | None, | 12 | end: int | None, |
| 13 | ) -> None: | 13 | ) -> None: |
| 14 | if not exists(output): | 14 | if not exists(output_dir): |
| 15 | makedirs(output) | 15 | makedirs(output_dir) |
| 16 | elif not isdir(output): | 16 | elif not isdir(output_dir): |
| 17 | print("Output is not a directory") | 17 | print("Output is not a directory") |
| 18 | exit(1) | 18 | exit(1) |
| 19 | process = Popen( | 19 | process = Popen( |
| @@ -40,7 +40,7 @@ def cut( | |||
| 40 | *("-reset_timestamps", "1"), | 40 | *("-reset_timestamps", "1"), |
| 41 | *("-force_key_frames", f"expr:gte(t,n_forced*{duration})"), | 41 | *("-force_key_frames", f"expr:gte(t,n_forced*{duration})"), |
| 42 | # Output | 42 | # Output |
| 43 | join(output, "%d.mp4"), | 43 | join(output_dir, "%d.mp4"), |
| 44 | ] | 44 | ] |
| 45 | ) | 45 | ) |
| 46 | 46 | ||
diff --git a/video2story/uploader.py b/video2story/uploader.py index d61ca43..6a2af42 100644 --- a/video2story/uploader.py +++ b/video2story/uploader.py | |||
| @@ -27,7 +27,7 @@ def upload( | |||
| 27 | phone=phone, | 27 | phone=phone, |
| 28 | ) | 28 | ) |
| 29 | telegram.login() | 29 | telegram.login() |
| 30 | me = int(telegram.call_method("getMe", block=True).update["id"]) | 30 | me = int(telegram.call_method("getMe", block=True).update["id"]) # type:ignore |
| 31 | 31 | ||
| 32 | if users is not None: | 32 | if users is not None: |
| 33 | user_ids = [] | 33 | user_ids = [] |
