Clean Code
172
annotation/.gitignore
vendored
@@ -1,172 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# experiments
|
||||
experiments/**/*.png
|
||||
experiments/**/*.csv
|
||||
experiments/**/*.mp4
|
||||
experiments/**/*.jsonl
|
||||
experiments/**/*.json
|
||||
experiments/**/*.md
|
||||
experiments/**/*.txt
|
||||
|
||||
# macos
|
||||
*DS_Store*
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 DuckAI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,50 +0,0 @@
|
||||
# OBS Setup
|
||||
|
||||
These are instructions on setting up OBS (Open Broadcaster Software) to record screen activity for creating the multimodal computer dataset.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to the OBS Project website: [https://obsproject.com/](https://obsproject.com/).
|
||||
2. Choose the appropriate installer for your operating system.
|
||||
3.
|
||||

|
||||
|
||||
3. Run the installer from your downloads folder and grant OBS the necessary permissions for installation.
|
||||
|
||||

|
||||
|
||||
4. Keep the default settings and proceed through the installation wizard by clicking "Next" and then "Finish."
|
||||
|
||||

|
||||
|
||||
5. OBS should now be open. If not, search for and open the application.
|
||||
|
||||

|
||||
|
||||
## Enabling OBS WebSocket Server
|
||||
|
||||
1. Click on "Tools" in the Navigation Bar within OBS, and then select "WebSocket Server Settings." A pop-up window will appear.
|
||||
|
||||

|
||||
|
||||
2. Check the box next to "Enable WebSocket server" and uncheck the box next to "Enable Authentication." Click "Apply," then "Ok." You should return to the main OBS page.
|
||||
Make sure the port is set to 4455.
|
||||

|
||||
|
||||
## Adding Display Capture and Recording
|
||||
|
||||
1. Now, back on the home page of OBS, select "Scene." Under "Sources," click the "+" button and then click "Display Capture." (in MacOS this is MacOS Screen Capture)
|
||||
|
||||

|
||||
|
||||
2. Select "Ok."
|
||||
|
||||

|
||||
|
||||
3. Make sure the "Display" is set to your main display, and you should see your screen on the canvas. Select "Ok." _(in MacOS if your screen is black with a red square in the top left try to disable then re-enable OBS Screen Recording permissions, this has worked before)_
|
||||
|
||||

|
||||
|
||||
4. Now you can close OBS and OBS will opened and controlled automatically when you launch the Computer Tracker App. Also, the Computer Tracker app creates a new OBS profile so you don't have to worry about your previous settings being messed up.
|
||||
|
||||

|
||||
@@ -1,98 +0,0 @@
|
||||
# DuckTrack
|
||||
|
||||
This is the repository for the DuckAI DuckTrack app which records all keyboard and mouse input as well as the screen for use in a multimodal computer interaction dataset.
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Download Application
|
||||
|
||||
<!-- TODO: add prebuilt applications in github releases -->
|
||||
Download the pre-built application for your system [here](https://github.com/TheDuckAI/DuckTrack/releases/).
|
||||
|
||||
Make sure you have OBS downloaded with the following configuration:
|
||||
1. Have a screen capture source recording your whole main screen.
|
||||
2. Enable desktop audio and mute microphone.
|
||||
3. Make sure the default websocket is enabled.
|
||||
|
||||
More detailed instructions for OBS setup and installation located [here](OBS_SETUP.md).
|
||||
|
||||
If you are on MacOS, make sure to enable to the following Privacy & Security permissions before running the app:
|
||||
|
||||
1. Accessibility (for playing back actions)
|
||||
2. Input Monitoring (for reading keyboard inputs)
|
||||
|
||||
Make sure to accept all other security permission dialogues to ensure that the app works properly.
|
||||
|
||||
### Build from source
|
||||
|
||||
Have Python >=3.11.
|
||||
|
||||
Clone this repo and `cd` into it:
|
||||
```bash
|
||||
$ git clone https://github.com/TheDuckAI/DuckTrack
|
||||
$ cd DuckTrack
|
||||
```
|
||||
|
||||
Install the dependencies for this project:
|
||||
```bash
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Build the application:
|
||||
```bash
|
||||
$ python3 build.py
|
||||
```
|
||||
|
||||
The built application should be located in the generated `dist` directory. After this, follow the remaining relevant setup instructions.
|
||||
|
||||
## Running the App
|
||||
|
||||
You can run the app like any other desktop app on your computer. If you decided to not download the app or build it from source, just run `python main.py` and it should work the same. You will be interacting with the app through an app tray icon or a small window.
|
||||
|
||||
### Recording
|
||||
|
||||
From the app tray or GUI, you can start and stop a recording as well as pause and resume a recording. Pausing and resuming is important for when you want to hide sensitive information like credit card of login credentials. You can optionally name your recording and give it a description upon stopping a recording. You can also view your recordings by pressing the "Show Recordings" option.
|
||||
|
||||
### Playback
|
||||
|
||||
You can playback a recording, i.e. simulate the series of events from the recording, by pressing "Play Latest Recording", which plays the latest created recording, or by pressing "Play Custom Recording", which lets you choose a recording to play. You can easily replay the most recently played recording by pressing "Replay Recording".
|
||||
|
||||
To stop the app mid-playback, just press `shift`+`esc` on your keyboard.
|
||||
|
||||
### Misc
|
||||
|
||||
To quit the app, you just press the "Quit" option.
|
||||
|
||||
## Recording Format
|
||||
|
||||
Recordings are stored in `Documents/DuckTrack_Recordings`. Each recording is a directory containing:
|
||||
|
||||
1. `events.jsonl` file - sequence of all computer actions that happened. A sample event may look like this:
|
||||
```json
|
||||
{"time_stamp": 1234567.89, "action": "move", "x": 69.0, "y": 420.0}
|
||||
```
|
||||
1. `metadata.json` - stores metadata about the computer that made the recording
|
||||
2. `README.md` - stores the description for the recording
|
||||
3. MP4 file - the screen recording from OBS of the recording.
|
||||
|
||||
Here is a [sample recording](example) for further reference.
|
||||
|
||||
## Technical Overview
|
||||
|
||||
<!-- maybe put a nice graphical representation of the app here -->
|
||||
|
||||
*TDB*
|
||||
|
||||
## Known Bugs
|
||||
|
||||
- After doing lots of playbacks on macOS, a segfault will occur.
|
||||
- Mouse movement is not captured when the current application is using raw input, i.e. video games.
|
||||
- OBS may not open in the background properly on some Linux machines.
|
||||
|
||||
## Things To Do
|
||||
|
||||
- Add logging
|
||||
- Testing
|
||||
- CI (with builds and testing)
|
||||
- Add way to hide/show window from the app tray (and it saves that as a preference?)
|
||||
- Make saving preferences a thing generally, like with natural scrolling too
|
||||
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,27 +0,0 @@
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
project_dir = Path(".")
|
||||
assets_dir = project_dir / "assets"
|
||||
main_py = project_dir / "main.py"
|
||||
icon_file = assets_dir / ("duck.ico" if system() == "Windows" else "duck.png")
|
||||
|
||||
for dir_to_remove in ["dist", "build"]:
|
||||
dir_path = project_dir / dir_to_remove
|
||||
if dir_path.exists():
|
||||
shutil.rmtree(dir_path)
|
||||
|
||||
pyinstaller_cmd = [
|
||||
"pyinstaller", "--onefile", "--windowed",
|
||||
f"--add-data={assets_dir}{';' if system() == 'Windows' else ':'}{assets_dir}",
|
||||
f"--name=DuckTrack", f"--icon={icon_file}", str(main_py)
|
||||
]
|
||||
|
||||
try:
|
||||
run(pyinstaller_cmd, check=True)
|
||||
except CalledProcessError as e:
|
||||
print("An error occurred while running PyInstaller:", e)
|
||||
sys.exit(1)
|
||||
@@ -1 +0,0 @@
|
||||
from .app import MainInterface
|
||||
@@ -1,251 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from platform import system
|
||||
|
||||
from PyQt6.QtCore import QTimer, pyqtSlot
|
||||
from PyQt6.QtGui import QAction, QIcon
|
||||
from PyQt6.QtWidgets import (QApplication, QCheckBox, QDialog, QFileDialog,
|
||||
QFormLayout, QLabel, QLineEdit, QMenu,
|
||||
QMessageBox, QPushButton, QSystemTrayIcon,
|
||||
QTextEdit, QVBoxLayout, QWidget)
|
||||
|
||||
from .obs_client import close_obs, is_obs_running, open_obs
|
||||
from .playback import Player, get_latest_recording
|
||||
from .recorder import Recorder
|
||||
from .util import get_recordings_dir, open_file
|
||||
|
||||
|
||||
class TitleDescriptionDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Recording Details")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.form_layout = QFormLayout()
|
||||
|
||||
self.title_label = QLabel("Title:")
|
||||
self.title_input = QLineEdit(self)
|
||||
self.form_layout.addRow(self.title_label, self.title_input)
|
||||
|
||||
self.description_label = QLabel("Description:")
|
||||
self.description_input = QTextEdit(self)
|
||||
self.form_layout.addRow(self.description_label, self.description_input)
|
||||
|
||||
layout.addLayout(self.form_layout)
|
||||
|
||||
self.submit_button = QPushButton("Save", self)
|
||||
self.submit_button.clicked.connect(self.accept)
|
||||
layout.addWidget(self.submit_button)
|
||||
|
||||
def get_values(self):
|
||||
return self.title_input.text(), self.description_input.toPlainText()
|
||||
|
||||
class MainInterface(QWidget):
|
||||
def __init__(self, app: QApplication):
|
||||
super().__init__()
|
||||
self.tray = QSystemTrayIcon(QIcon(resource_path("assets/duck.png")))
|
||||
self.tray.show()
|
||||
|
||||
self.app = app
|
||||
|
||||
self.init_tray()
|
||||
self.init_window()
|
||||
|
||||
if not is_obs_running():
|
||||
self.obs_process = open_obs()
|
||||
|
||||
def init_window(self):
|
||||
self.setWindowTitle("DuckTrack")
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.toggle_record_button = QPushButton("Start Recording", self)
|
||||
self.toggle_record_button.clicked.connect(self.toggle_record)
|
||||
layout.addWidget(self.toggle_record_button)
|
||||
|
||||
self.toggle_pause_button = QPushButton("Pause Recording", self)
|
||||
self.toggle_pause_button.clicked.connect(self.toggle_pause)
|
||||
self.toggle_pause_button.setEnabled(False)
|
||||
layout.addWidget(self.toggle_pause_button)
|
||||
|
||||
self.show_recordings_button = QPushButton("Show Recordings", self)
|
||||
self.show_recordings_button.clicked.connect(lambda: open_file(get_recordings_dir()))
|
||||
layout.addWidget(self.show_recordings_button)
|
||||
|
||||
self.play_latest_button = QPushButton("Play Latest Recording", self)
|
||||
self.play_latest_button.clicked.connect(self.play_latest_recording)
|
||||
layout.addWidget(self.play_latest_button)
|
||||
|
||||
self.play_custom_button = QPushButton("Play Custom Recording", self)
|
||||
self.play_custom_button.clicked.connect(self.play_custom_recording)
|
||||
layout.addWidget(self.play_custom_button)
|
||||
|
||||
self.replay_recording_button = QPushButton("Replay Recording", self)
|
||||
self.replay_recording_button.clicked.connect(self.replay_recording)
|
||||
self.replay_recording_button.setEnabled(False)
|
||||
layout.addWidget(self.replay_recording_button)
|
||||
|
||||
self.quit_button = QPushButton("Quit", self)
|
||||
self.quit_button.clicked.connect(self.quit)
|
||||
layout.addWidget(self.quit_button)
|
||||
|
||||
self.natural_scrolling_checkbox = QCheckBox("Natural Scrolling", self, checked=system() == "Darwin")
|
||||
layout.addWidget(self.natural_scrolling_checkbox)
|
||||
|
||||
self.natural_scrolling_checkbox.stateChanged.connect(self.toggle_natural_scrolling)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def init_tray(self):
|
||||
self.menu = QMenu()
|
||||
self.tray.setContextMenu(self.menu)
|
||||
|
||||
self.toggle_record_action = QAction("Start Recording")
|
||||
self.toggle_record_action.triggered.connect(self.toggle_record)
|
||||
self.menu.addAction(self.toggle_record_action)
|
||||
|
||||
self.toggle_pause_action = QAction("Pause Recording")
|
||||
self.toggle_pause_action.triggered.connect(self.toggle_pause)
|
||||
self.toggle_pause_action.setVisible(False)
|
||||
self.menu.addAction(self.toggle_pause_action)
|
||||
|
||||
self.show_recordings_action = QAction("Show Recordings")
|
||||
self.show_recordings_action.triggered.connect(lambda: open_file(get_recordings_dir()))
|
||||
self.menu.addAction(self.show_recordings_action)
|
||||
|
||||
self.play_latest_action = QAction("Play Latest Recording")
|
||||
self.play_latest_action.triggered.connect(self.play_latest_recording)
|
||||
self.menu.addAction(self.play_latest_action)
|
||||
|
||||
self.play_custom_action = QAction("Play Custom Recording")
|
||||
self.play_custom_action.triggered.connect(self.play_custom_recording)
|
||||
self.menu.addAction(self.play_custom_action)
|
||||
|
||||
self.replay_recording_action = QAction("Replay Recording")
|
||||
self.replay_recording_action.triggered.connect(self.replay_recording)
|
||||
self.menu.addAction(self.replay_recording_action)
|
||||
self.replay_recording_action.setVisible(False)
|
||||
|
||||
self.quit_action = QAction("Quit")
|
||||
self.quit_action.triggered.connect(self.quit)
|
||||
self.menu.addAction(self.quit_action)
|
||||
|
||||
self.menu.addSeparator()
|
||||
|
||||
self.natural_scrolling_option = QAction("Natural Scrolling", checkable=True, checked=system() == "Darwin")
|
||||
self.natural_scrolling_option.triggered.connect(self.toggle_natural_scrolling)
|
||||
self.menu.addAction(self.natural_scrolling_option)
|
||||
|
||||
@pyqtSlot()
|
||||
def replay_recording(self):
|
||||
player = Player()
|
||||
if hasattr(self, "last_played_recording_path"):
|
||||
player.play(self.last_played_recording_path)
|
||||
else:
|
||||
self.display_error_message("No recording has been played yet!")
|
||||
|
||||
@pyqtSlot()
|
||||
def play_latest_recording(self):
|
||||
player = Player()
|
||||
recording_path = get_latest_recording()
|
||||
self.last_played_recording_path = recording_path
|
||||
self.replay_recording_action.setVisible(True)
|
||||
self.replay_recording_button.setEnabled(True)
|
||||
player.play(recording_path)
|
||||
|
||||
@pyqtSlot()
|
||||
def play_custom_recording(self):
|
||||
player = Player()
|
||||
directory = QFileDialog.getExistingDirectory(None, "Select Recording", get_recordings_dir())
|
||||
if directory:
|
||||
self.last_played_recording_path = directory
|
||||
self.replay_recording_button.setEnabled(True)
|
||||
self.replay_recording_action.setVisible(True)
|
||||
player.play(directory)
|
||||
|
||||
@pyqtSlot()
|
||||
def quit(self):
|
||||
if hasattr(self, "recorder_thread"):
|
||||
self.toggle_record()
|
||||
if hasattr(self, "obs_process"):
|
||||
close_obs(self.obs_process)
|
||||
self.app.quit()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.quit()
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_natural_scrolling(self):
|
||||
sender = self.sender()
|
||||
|
||||
if sender == self.natural_scrolling_checkbox:
|
||||
state = self.natural_scrolling_checkbox.isChecked()
|
||||
self.natural_scrolling_option.setChecked(state)
|
||||
else:
|
||||
state = self.natural_scrolling_option.isChecked()
|
||||
self.natural_scrolling_checkbox.setChecked(state)
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_pause(self):
|
||||
if self.recorder_thread._is_paused:
|
||||
self.recorder_thread.resume_recording()
|
||||
self.toggle_pause_action.setText("Pause Recording")
|
||||
self.toggle_pause_button.setText("Pause Recording")
|
||||
else:
|
||||
self.recorder_thread.pause_recording()
|
||||
self.toggle_pause_action.setText("Resume Recording")
|
||||
self.toggle_pause_button.setText("Resume Recording")
|
||||
|
||||
@pyqtSlot()
|
||||
def toggle_record(self):
|
||||
if not hasattr(self, "recorder_thread"):
|
||||
self.recorder_thread = Recorder(natural_scrolling=self.natural_scrolling_checkbox.isChecked())
|
||||
self.recorder_thread.recording_stopped.connect(self.on_recording_stopped)
|
||||
self.recorder_thread.start()
|
||||
self.update_menu(True)
|
||||
else:
|
||||
self.recorder_thread.stop_recording()
|
||||
self.recorder_thread.terminate()
|
||||
|
||||
recording_dir = self.recorder_thread.recording_path
|
||||
|
||||
del self.recorder_thread
|
||||
|
||||
dialog = TitleDescriptionDialog()
|
||||
QTimer.singleShot(0, dialog.raise_)
|
||||
result = dialog.exec()
|
||||
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
title, description = dialog.get_values()
|
||||
|
||||
if title:
|
||||
renamed_dir = os.path.join(os.path.dirname(recording_dir), title)
|
||||
os.rename(recording_dir, renamed_dir)
|
||||
|
||||
with open(os.path.join(renamed_dir, 'README.md'), 'w') as f:
|
||||
f.write(description)
|
||||
|
||||
self.on_recording_stopped()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_recording_stopped(self):
|
||||
self.update_menu(False)
|
||||
|
||||
def update_menu(self, is_recording: bool):
|
||||
self.toggle_record_button.setText("Stop Recording" if is_recording else "Start Recording")
|
||||
self.toggle_record_action.setText("Stop Recording" if is_recording else "Start Recording")
|
||||
|
||||
self.toggle_pause_button.setEnabled(is_recording)
|
||||
self.toggle_pause_action.setVisible(is_recording)
|
||||
|
||||
def display_error_message(self, message):
|
||||
QMessageBox.critical(None, "Error", message)
|
||||
|
||||
def resource_path(relative_path: str) -> str:
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = getattr(sys, "_MEIPASS")
|
||||
else:
|
||||
base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
@@ -1,33 +0,0 @@
|
||||
from pynput.keyboard import Listener
|
||||
|
||||
from .util import name_to_key
|
||||
|
||||
|
||||
class KeyCombinationListener:
|
||||
"""
|
||||
Simple and bad key combination listener.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.current_keys = set()
|
||||
self.callbacks = {}
|
||||
self.listener = Listener(on_press=self.on_key_press, on_release=self.on_key_release)
|
||||
|
||||
def add_comb(self, keys, callback):
|
||||
self.callbacks[tuple([name_to_key(key_name) for key_name in sorted(keys)])] = callback
|
||||
|
||||
def on_key_press(self, key):
|
||||
self.current_keys.add(key)
|
||||
for comb, callback in self.callbacks.items():
|
||||
if all(k in self.current_keys for k in comb):
|
||||
return callback()
|
||||
|
||||
def on_key_release(self, key):
|
||||
if key in self.current_keys:
|
||||
self.current_keys.remove(key)
|
||||
|
||||
def start(self):
|
||||
self.listener.start()
|
||||
|
||||
def stop(self):
|
||||
self.listener.stop()
|
||||
@@ -1,60 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from platform import uname
|
||||
|
||||
from screeninfo import get_monitors
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
"""
|
||||
Handles various system metadata collection.
|
||||
"""
|
||||
|
||||
def __init__(self, recording_path: str, natural_scrolling: bool):
|
||||
self.recording_path = recording_path
|
||||
|
||||
self.metadata = uname()._asdict()
|
||||
|
||||
self.metadata["id"] = uuid.getnode()
|
||||
|
||||
main_monitor = get_monitors()[0]
|
||||
self.metadata["screen_width"] = main_monitor.width
|
||||
self.metadata["screen_height"] = main_monitor.height
|
||||
|
||||
try:
|
||||
match self.metadata["system"]:
|
||||
case "Windows":
|
||||
import wmi
|
||||
for item in wmi.WMI().Win32_ComputerSystem():
|
||||
self.metadata["model"] = item.Model
|
||||
break
|
||||
case "Darwin":
|
||||
import subprocess
|
||||
model = subprocess.check_output(["sysctl", "-n", "hw.model"]).decode().strip()
|
||||
self.metadata["model"] = model
|
||||
case "Linux":
|
||||
with open("/sys/devices/virtual/dmi/id/product_name", "r") as f:
|
||||
self.metadata["model"] = f.read().strip()
|
||||
except:
|
||||
self.metadata["model"] = "Unknown"
|
||||
|
||||
self.metadata["scroll_direction"] = -1 if natural_scrolling else 1
|
||||
|
||||
def save_metadata(self):
|
||||
metadata_path = os.path.join(self.recording_path, "metadata.json")
|
||||
with open(metadata_path, "w") as f:
|
||||
json.dump(self.metadata, f, indent=4)
|
||||
|
||||
def collect(self):
|
||||
self.metadata["start_time"] = self._get_time_stamp()
|
||||
|
||||
def end_collect(self):
|
||||
self.metadata["stop_time"] = self._get_time_stamp()
|
||||
|
||||
def add_obs_record_state_timings(self, record_state_events: dict[str, float]):
|
||||
self.metadata["obs_record_state_timings"] = record_state_events
|
||||
|
||||
def _get_time_stamp(self):
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -1,200 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from platform import system
|
||||
|
||||
import obsws_python as obs
|
||||
import psutil
|
||||
|
||||
|
||||
def is_obs_running() -> bool:
|
||||
try:
|
||||
for process in psutil.process_iter(attrs=["pid", "name"]):
|
||||
if "obs" in process.info["name"].lower():
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
raise Exception("Could not check if OBS is running already. Please check manually.")
|
||||
|
||||
def close_obs(obs_process: subprocess.Popen):
|
||||
if obs_process:
|
||||
obs_process.terminate()
|
||||
try:
|
||||
obs_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
obs_process.kill()
|
||||
|
||||
def find_obs() -> str:
|
||||
common_paths = {
|
||||
"Windows": [
|
||||
"C:\\Program Files\\obs-studio\\bin\\64bit\\obs64.exe",
|
||||
"C:\\Program Files (x86)\\obs-studio\\bin\\32bit\\obs32.exe"
|
||||
],
|
||||
"Darwin": [
|
||||
"/Applications/OBS.app/Contents/MacOS/OBS",
|
||||
"/opt/homebrew/bin/obs"
|
||||
],
|
||||
"Linux": [
|
||||
"/usr/bin/obs",
|
||||
"/usr/local/bin/obs"
|
||||
]
|
||||
}
|
||||
|
||||
for path in common_paths.get(system(), []):
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
try:
|
||||
if system() == "Windows":
|
||||
obs_path = subprocess.check_output("where obs", shell=True).decode().strip()
|
||||
else:
|
||||
obs_path = subprocess.check_output("which obs", shell=True).decode().strip()
|
||||
|
||||
if os.path.exists(obs_path):
|
||||
return obs_path
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
return "obs"
|
||||
|
||||
def open_obs() -> subprocess.Popen:
|
||||
try:
|
||||
obs_path = find_obs()
|
||||
if system() == "Windows":
|
||||
# you have to change the working directory first for OBS to find the correct locale on windows
|
||||
os.chdir(os.path.dirname(obs_path))
|
||||
obs_path = os.path.basename(obs_path)
|
||||
return subprocess.Popen([obs_path, "--startreplaybuffer", "--minimize-to-tray"])
|
||||
except:
|
||||
raise Exception("Failed to find OBS, please open OBS manually.")
|
||||
|
||||
class OBSClient:
|
||||
"""
|
||||
Controls the OBS client via the OBS websocket.
|
||||
Sets all the correct settings for recording.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
recording_path: str,
|
||||
metadata: dict,
|
||||
fps=30,
|
||||
output_width=1280,
|
||||
output_height=720,
|
||||
):
|
||||
self.metadata = metadata
|
||||
|
||||
self.req_client = obs.ReqClient()
|
||||
self.event_client = obs.EventClient()
|
||||
|
||||
self.record_state_events = {}
|
||||
|
||||
def on_record_state_changed(data):
|
||||
output_state = data.output_state
|
||||
print("record state changed:", output_state)
|
||||
if output_state not in self.record_state_events:
|
||||
self.record_state_events[output_state] = []
|
||||
self.record_state_events[output_state].append(time.perf_counter())
|
||||
|
||||
self.event_client.callback.register(on_record_state_changed)
|
||||
|
||||
self.old_profile = self.req_client.get_profile_list().current_profile_name
|
||||
|
||||
if "computer_tracker" not in self.req_client.get_profile_list().profiles:
|
||||
self.req_client.create_profile("computer_tracker")
|
||||
else:
|
||||
self.req_client.set_current_profile("computer_tracker")
|
||||
self.req_client.create_profile("temp")
|
||||
self.req_client.remove_profile("temp")
|
||||
self.req_client.set_current_profile("computer_tracker")
|
||||
|
||||
base_width = metadata["screen_width"]
|
||||
base_height = metadata["screen_height"]
|
||||
|
||||
if metadata["system"] == "Darwin":
|
||||
# for retina displays
|
||||
# TODO: check if external displays are messed up by this
|
||||
base_width *= 2
|
||||
base_height *= 2
|
||||
|
||||
scaled_width, scaled_height = _scale_resolution(base_width, base_height, output_width, output_height)
|
||||
|
||||
self.req_client.set_profile_parameter("Video", "BaseCX", str(base_width))
|
||||
self.req_client.set_profile_parameter("Video", "BaseCY", str(base_height))
|
||||
self.req_client.set_profile_parameter("Video", "OutputCX", str(scaled_width))
|
||||
self.req_client.set_profile_parameter("Video", "OutputCY", str(scaled_height))
|
||||
self.req_client.set_profile_parameter("Video", "ScaleType", "lanczos")
|
||||
|
||||
self.req_client.set_profile_parameter("AdvOut", "RescaleRes", f"{base_width}x{base_height}")
|
||||
self.req_client.set_profile_parameter("AdvOut", "RecRescaleRes", f"{base_width}x{base_height}")
|
||||
self.req_client.set_profile_parameter("AdvOut", "FFRescaleRes", f"{base_width}x{base_height}")
|
||||
|
||||
self.req_client.set_profile_parameter("Video", "FPSCommon", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSInt", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSNum", str(fps))
|
||||
self.req_client.set_profile_parameter("Video", "FPSDen", "1")
|
||||
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "RecFormat2", "mp4")
|
||||
|
||||
bitrate = int(_get_bitrate_mbps(scaled_width, scaled_height, fps=fps) * 1000 / 50) * 50
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "VBitrate", str(bitrate))
|
||||
|
||||
# do this in order to get pause & resume
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "RecQuality", "Small")
|
||||
|
||||
self.req_client.set_profile_parameter("SimpleOutput", "FilePath", recording_path)
|
||||
|
||||
# TODO: not all OBS configs have this, maybe just instruct the user to mute themselves
|
||||
|
||||
|
||||
try:
|
||||
self.req_client.set_input_mute("Mic/Aux", muted=True)
|
||||
except obs.error.OBSSDKRequestError :
|
||||
# In case there is no Mic/Aux input, this will throw an error
|
||||
pass
|
||||
|
||||
def start_recording(self):
|
||||
self.req_client.start_record()
|
||||
|
||||
def stop_recording(self):
|
||||
self.req_client.stop_record()
|
||||
self.req_client.set_current_profile(self.old_profile) # restore old profile
|
||||
|
||||
def pause_recording(self):
|
||||
self.req_client.pause_record()
|
||||
|
||||
def resume_recording(self):
|
||||
self.req_client.resume_record()
|
||||
|
||||
def _get_bitrate_mbps(width: int, height: int, fps=30) -> float:
|
||||
"""
|
||||
Gets the YouTube recommended bitrate in Mbps for a given resolution and framerate.
|
||||
Refer to https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
|
||||
"""
|
||||
resolutions = {
|
||||
(7680, 4320): {30: 120, 60: 180},
|
||||
(3840, 2160): {30: 40, 60: 60.5},
|
||||
(2160, 1440): {30: 16, 60: 24},
|
||||
(1920, 1080): {30: 8, 60: 12},
|
||||
(1280, 720): {30: 5, 60: 7.5},
|
||||
(640, 480): {30: 2.5, 60: 4},
|
||||
(480, 360): {30: 1, 60: 1.5}
|
||||
}
|
||||
|
||||
if (width, height) in resolutions:
|
||||
return resolutions[(width, height)].get(fps)
|
||||
else:
|
||||
# approximate the bitrate using a simple linear model
|
||||
area = width * height
|
||||
multiplier = 3.5982188179592543e-06 if fps == 30 else 5.396175171097084e-06
|
||||
constant = 2.418399836285939 if fps == 30 else 3.742780056500365
|
||||
return multiplier * area + constant
|
||||
|
||||
def _scale_resolution(base_width: int, base_height: int, target_width: int, target_height: int) -> tuple[int, int]:
|
||||
target_area = target_width * target_height
|
||||
aspect_ratio = base_width / base_height
|
||||
|
||||
scaled_height = int((target_area / aspect_ratio) ** 0.5)
|
||||
scaled_width = int(aspect_ratio * scaled_height)
|
||||
|
||||
return scaled_width, scaled_height
|
||||
@@ -1,188 +0,0 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pyautogui
|
||||
from pynput.keyboard import Controller as KeyboardController
|
||||
from pynput.keyboard import Key
|
||||
from pynput.mouse import Button
|
||||
from pynput.mouse import Controller as MouseController
|
||||
|
||||
from .keycomb import KeyCombinationListener
|
||||
from .util import (fix_windows_dpi_scaling, get_recordings_dir, name_to_button,
|
||||
name_to_key)
|
||||
|
||||
pyautogui.PAUSE = 0
|
||||
pyautogui.DARWIN_CATCH_UP_TIME = 0
|
||||
|
||||
class Player:
|
||||
"""
|
||||
Plays back recordings.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.stop_playback = False
|
||||
self.listener = KeyCombinationListener()
|
||||
|
||||
def stop_comb_pressed():
|
||||
self.stop_playback = True
|
||||
return False
|
||||
|
||||
self.listener.add_comb(("shift", "esc"), stop_comb_pressed)
|
||||
self.listener.start()
|
||||
|
||||
def play(self, recording_path: str):
|
||||
with open(os.path.join(recording_path, "events.jsonl"), "r") as f:
|
||||
events = [json.loads(line) for line in f.readlines()]
|
||||
|
||||
with open(os.path.join(recording_path, "metadata.json"), "r") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
self.playback(events, metadata)
|
||||
|
||||
def playback(self, events: list[dict], metadata: dict):
|
||||
if metadata["system"] == "Windows":
|
||||
fix_windows_dpi_scaling()
|
||||
|
||||
mouse_controller = MouseController()
|
||||
keyboard_controller = KeyboardController()
|
||||
|
||||
if not events:
|
||||
self.listener.stop()
|
||||
return
|
||||
|
||||
presses_to_skip = 0
|
||||
releases_to_skip = 0
|
||||
|
||||
in_click_sequence = False
|
||||
|
||||
for i, event in enumerate(events):
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if self.stop_playback:
|
||||
return
|
||||
|
||||
def do_mouse_press(button):
|
||||
for j, second_event in enumerate(events[i+1:]):
|
||||
# make sure the time between mouse clicks is less than 500ms
|
||||
if second_event["time_stamp"] - event["time_stamp"] > 0.5:
|
||||
break
|
||||
|
||||
if "x" in second_event and "y" in second_event:
|
||||
# if the mouse moves out of the click radius/rectangle, it is not a click sequence
|
||||
if math.sqrt((second_event["y"] - event["y"]) ** 2 +
|
||||
(second_event["x"] - event["x"]) ** 2) > 4:
|
||||
break
|
||||
|
||||
if second_event["action"] == "click" and second_event["pressed"]:
|
||||
for k, third_event in enumerate(events[i+j+2:]):
|
||||
if third_event["time_stamp"] - second_event["time_stamp"] > 0.5:
|
||||
break
|
||||
|
||||
if "x" in third_event and "y" in third_event:
|
||||
if math.sqrt((third_event["y"] - event["y"]) ** 2 +
|
||||
(third_event["x"] - event["x"]) ** 2) > 5:
|
||||
break
|
||||
|
||||
if third_event["action"] == "click" and third_event["pressed"]:
|
||||
mouse_controller.click(button, 3)
|
||||
return 2, 2
|
||||
|
||||
mouse_controller.click(button, 2)
|
||||
return 1, 1
|
||||
|
||||
mouse_controller.press(button)
|
||||
return 0, 0
|
||||
|
||||
if event["action"] == "move":
|
||||
mouse_controller.position = (event["x"], event["y"])
|
||||
|
||||
elif event["action"] == "click":
|
||||
button = name_to_button(event["button"])
|
||||
|
||||
if event["pressed"]:
|
||||
if presses_to_skip == 0:
|
||||
presses, releases = do_mouse_press(button)
|
||||
presses_to_skip += presses
|
||||
releases_to_skip += releases
|
||||
|
||||
if presses > 0:
|
||||
in_click_sequence = True
|
||||
else:
|
||||
presses_to_skip -= 1
|
||||
else:
|
||||
if releases_to_skip == 0:
|
||||
mouse_controller.release(button)
|
||||
|
||||
if in_click_sequence:
|
||||
keyboard_controller.press(Key.shift)
|
||||
mouse_controller.click(Button.left)
|
||||
keyboard_controller.release(Key.shift)
|
||||
in_click_sequence = False
|
||||
else:
|
||||
releases_to_skip -= 1
|
||||
|
||||
elif event["action"] == "scroll":
|
||||
if metadata["system"] == "Windows":
|
||||
# for some reason on windows, pynput scroll is correct but pyautogui is not
|
||||
mouse_controller.scroll(metadata["scroll_direction"] * event["dx"], metadata["scroll_direction"] * event["dy"])
|
||||
else:
|
||||
pyautogui.hscroll(clicks=metadata["scroll_direction"] * event["dx"])
|
||||
pyautogui.vscroll(clicks=metadata["scroll_direction"] * event["dy"])
|
||||
|
||||
elif event["action"] in ["press", "release"]:
|
||||
key = name_to_key(event["name"])
|
||||
if event["action"] == "press":
|
||||
keyboard_controller.press(key)
|
||||
else:
|
||||
keyboard_controller.release(key)
|
||||
|
||||
# sleep for the correct amount of time
|
||||
|
||||
end_time = time.perf_counter()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
if i + 1 < len(events):
|
||||
desired_delay = events[i + 1]["time_stamp"] - event["time_stamp"]
|
||||
delay = desired_delay - execution_time
|
||||
if delay < 0:
|
||||
print(f"warning: behind by {-delay * 1000:.3f} ms")
|
||||
elif delay != 0:
|
||||
wait_until = time.perf_counter() + delay
|
||||
while time.perf_counter() < wait_until:
|
||||
pass
|
||||
|
||||
self.listener.stop()
|
||||
|
||||
def get_latest_recording() -> str:
|
||||
recordings_dir = get_recordings_dir()
|
||||
if not os.path.exists(recordings_dir):
|
||||
raise Exception("The recordings directory does not exist")
|
||||
|
||||
recordings = [os.path.join(recordings_dir, f) for f in os.listdir(recordings_dir) if os.path.isdir(os.path.join(recordings_dir, f))]
|
||||
|
||||
if len(recordings) == 0:
|
||||
raise Exception("You have no recordings to play back")
|
||||
|
||||
latest_recording = max(recordings, key=os.path.getctime)
|
||||
|
||||
return latest_recording
|
||||
|
||||
def main():
|
||||
player = Player()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
recording_path = sys.argv[1]
|
||||
else:
|
||||
recording_path = get_latest_recording()
|
||||
|
||||
player.play(recording_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = 3
|
||||
print("press shift+esc to stop the playback")
|
||||
print(f"starting in {n} seconds...")
|
||||
time.sleep(n)
|
||||
main()
|
||||
@@ -1,145 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from platform import system
|
||||
from queue import Queue
|
||||
|
||||
from pynput import keyboard, mouse
|
||||
from pynput.keyboard import KeyCode
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
from .metadata import MetadataManager
|
||||
from .obs_client import OBSClient
|
||||
from .util import fix_windows_dpi_scaling, get_recordings_dir
|
||||
|
||||
|
||||
class Recorder(QThread):
|
||||
"""
|
||||
Makes recordings.
|
||||
"""
|
||||
|
||||
recording_stopped = pyqtSignal()
|
||||
|
||||
def __init__(self, natural_scrolling: bool):
|
||||
super().__init__()
|
||||
|
||||
if system() == "Windows":
|
||||
fix_windows_dpi_scaling()
|
||||
|
||||
self.recording_path = self._get_recording_path()
|
||||
|
||||
self._is_recording = False
|
||||
self._is_paused = False
|
||||
|
||||
self.event_queue = Queue()
|
||||
self.events_file = open(os.path.join(self.recording_path, "events.jsonl"), "a")
|
||||
|
||||
self.metadata_manager = MetadataManager(
|
||||
recording_path=self.recording_path,
|
||||
natural_scrolling=natural_scrolling
|
||||
)
|
||||
self.obs_client = OBSClient(recording_path=self.recording_path,
|
||||
metadata=self.metadata_manager.metadata)
|
||||
|
||||
self.mouse_listener = mouse.Listener(
|
||||
on_move=self.on_move,
|
||||
on_click=self.on_click,
|
||||
on_scroll=self.on_scroll)
|
||||
|
||||
self.keyboard_listener = keyboard.Listener(
|
||||
on_press=self.on_press,
|
||||
on_release=self.on_release)
|
||||
|
||||
def on_move(self, x, y):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "move",
|
||||
"x": x,
|
||||
"y": y}, block=False)
|
||||
|
||||
def on_click(self, x, y, button, pressed):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "click",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": button.name,
|
||||
"pressed": pressed}, block=False)
|
||||
|
||||
def on_scroll(self, x, y, dx, dy):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "scroll",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"dx": dx,
|
||||
"dy": dy}, block=False)
|
||||
|
||||
def on_press(self, key):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "press",
|
||||
"name": key.char if type(key) == KeyCode else key.name}, block=False)
|
||||
|
||||
def on_release(self, key):
|
||||
if not self._is_paused:
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "release",
|
||||
"name": key.char if type(key) == KeyCode else key.name}, block=False)
|
||||
|
||||
def run(self):
|
||||
self._is_recording = True
|
||||
|
||||
self.metadata_manager.collect()
|
||||
self.obs_client.start_recording()
|
||||
|
||||
self.mouse_listener.start()
|
||||
self.keyboard_listener.start()
|
||||
|
||||
while self._is_recording:
|
||||
event = self.event_queue.get()
|
||||
self.events_file.write(json.dumps(event) + "\n")
|
||||
|
||||
def stop_recording(self):
|
||||
if self._is_recording:
|
||||
self._is_recording = False
|
||||
|
||||
self.metadata_manager.end_collect()
|
||||
|
||||
self.mouse_listener.stop()
|
||||
self.keyboard_listener.stop()
|
||||
|
||||
self.obs_client.stop_recording()
|
||||
self.metadata_manager.add_obs_record_state_timings(self.obs_client.record_state_events)
|
||||
self.events_file.close()
|
||||
self.metadata_manager.save_metadata()
|
||||
|
||||
self.recording_stopped.emit()
|
||||
|
||||
def pause_recording(self):
|
||||
if not self._is_paused and self._is_recording:
|
||||
self._is_paused = True
|
||||
self.obs_client.pause_recording()
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "pause"}, block=False)
|
||||
|
||||
def resume_recording(self):
|
||||
if self._is_paused and self._is_recording:
|
||||
self._is_paused = False
|
||||
self.obs_client.resume_recording()
|
||||
self.event_queue.put({"time_stamp": time.perf_counter(),
|
||||
"action": "resume"}, block=False)
|
||||
|
||||
def _get_recording_path(self) -> str:
|
||||
recordings_dir = get_recordings_dir()
|
||||
|
||||
if not os.path.exists(recordings_dir):
|
||||
os.mkdir(recordings_dir)
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
recording_path = os.path.join(recordings_dir, f"recording-{current_time}")
|
||||
os.mkdir(recording_path)
|
||||
|
||||
return recording_path
|
||||
@@ -1,38 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from pynput.keyboard import Key, KeyCode
|
||||
from pynput.mouse import Button
|
||||
|
||||
|
||||
def name_to_key(name: str) -> Key | KeyCode:
|
||||
try:
|
||||
return getattr(Key, name)
|
||||
except AttributeError:
|
||||
return KeyCode.from_char(name)
|
||||
|
||||
def name_to_button(name: str) -> Button:
|
||||
return getattr(Button, name)
|
||||
|
||||
def get_recordings_dir() -> str:
|
||||
documents_folder = Path.home() / 'Documents' / 'DuckTrack_Recordings'
|
||||
return str(documents_folder)
|
||||
|
||||
def fix_windows_dpi_scaling():
|
||||
"""
|
||||
Fixes DPI scaling issues with legacy windows applications
|
||||
Reference: https://pynput.readthedocs.io/en/latest/mouse.html#ensuring-consistent-coordinates-between-listener-and-controller-on-windows
|
||||
"""
|
||||
import ctypes
|
||||
PROCESS_PER_MONITOR_DPI_AWARE = 2
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
|
||||
|
||||
def open_file(path):
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(path)
|
||||
elif platform.system() == "Darwin":
|
||||
subprocess.Popen(["open", path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", path])
|
||||
@@ -1,48 +0,0 @@
|
||||
import glob
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import seaborn as sns
|
||||
from scipy.stats import sem, t
|
||||
|
||||
|
||||
def calculate_confidence_interval(data, confidence=0.95):
|
||||
n = len(data)
|
||||
m = np.mean(data)
|
||||
std_err = sem(data)
|
||||
h = std_err * t.ppf((1 + confidence) / 2, n - 1)
|
||||
return m, m-h, m+h
|
||||
|
||||
runs = glob.glob("run*.txt")
|
||||
TOTAL_EVENTS = 22509
|
||||
percent_delays = []
|
||||
all_delays = []
|
||||
|
||||
for run in runs:
|
||||
with open(run, "r") as f:
|
||||
delays = [float(line.split()[3]) for line in f if float(line.split()[3]) > 0] # consider only positive delays
|
||||
percent_delays.append((len(delays) / TOTAL_EVENTS) * 100)
|
||||
all_delays.extend(delays)
|
||||
|
||||
average_percent_delays = np.mean(percent_delays)
|
||||
confidence_interval_percent_delays = calculate_confidence_interval(percent_delays)
|
||||
print(f"Average percentage of delayed events across all runs: {average_percent_delays:.2f}%")
|
||||
print(f"95% Confidence interval: ({confidence_interval_percent_delays[1]:.2f}%, {confidence_interval_percent_delays[2]:.2f}%)")
|
||||
|
||||
if all_delays:
|
||||
mean_delay = np.mean(all_delays)
|
||||
confidence_interval_delays = calculate_confidence_interval(all_delays)
|
||||
print(f"Mean delay time: {mean_delay:.2f}")
|
||||
print(f"95% Confidence interval for delay time: ({confidence_interval_delays[1]:.2f}, {confidence_interval_delays[2]:.2f})")
|
||||
else:
|
||||
print("No delay data available for calculation.")
|
||||
|
||||
sns.histplot(all_delays, bins=30, kde=False)
|
||||
plt.xlabel('Delay Time (ms)')
|
||||
plt.ylabel('Frequency')
|
||||
plt.yscale('log')
|
||||
plt.title('Histogram of Delay Times (macOS)')
|
||||
|
||||
plt.savefig('delays.png', dpi=300)
|
||||
|
||||
plt.show()
|
||||
@@ -1,110 +0,0 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import scipy.stats as stats
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
from tqdm import tqdm
|
||||
|
||||
# use this: https://sketch.io
|
||||
|
||||
def calculate_rmse(imageA, imageB):
|
||||
err = np.sum((imageA - imageB) ** 2)
|
||||
err /= float(imageA.shape[0] * imageA.shape[1])
|
||||
return np.sqrt(err)
|
||||
|
||||
def compare_images(ground_truth_path, sample_paths):
|
||||
results = []
|
||||
gt_image = cv2.imread(ground_truth_path, cv2.IMREAD_GRAYSCALE)
|
||||
|
||||
if gt_image is None:
|
||||
raise ValueError("Ground truth image could not be read. Please check the file path.")
|
||||
|
||||
gt_image = gt_image.astype("float") / 255.0
|
||||
|
||||
for path in tqdm(sample_paths):
|
||||
sample_image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
||||
|
||||
if sample_image is None:
|
||||
print(f"WARNING: Sample image at path {path} could not be read. Skipping this image.")
|
||||
continue
|
||||
|
||||
sample_image = sample_image.astype("float") / 255.0
|
||||
|
||||
rmse_value = calculate_rmse(gt_image, sample_image)
|
||||
ssim_value, _ = ssim(gt_image, sample_image, full=True, data_range=1) # Corrected line
|
||||
|
||||
diff_mask = cv2.absdiff(gt_image, sample_image)
|
||||
|
||||
# plt.imshow(diff_mask * 255, cmap='gray')
|
||||
# plt.title(f'Difference Mask for {os.path.basename(path)}\nRMSE: {rmse_value:.5f} - SSIM: {ssim_value:.5f}')
|
||||
# plt.show()
|
||||
|
||||
results.append({
|
||||
'path': path,
|
||||
'rmse': rmse_value,
|
||||
'ssim': ssim_value,
|
||||
'diff_mask': diff_mask
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
ground_truth = 'ground_truth.png'
|
||||
sample_images = glob.glob("samples/*.png")
|
||||
|
||||
results = compare_images(ground_truth, sample_images)
|
||||
|
||||
for res in results:
|
||||
print(f"Image: {res['path']} - RMSE: {res['rmse']} - SSIM: {res['ssim']}")
|
||||
|
||||
def calculate_confidence_interval(data, confidence_level=0.95):
|
||||
mean = np.mean(data)
|
||||
sem = stats.sem(data)
|
||||
df = len(data) - 1
|
||||
me = sem * stats.t.ppf((1 + confidence_level) / 2, df)
|
||||
return mean - me, mean + me
|
||||
|
||||
rmse_values = [res['rmse'] for res in results]
|
||||
ssim_values = [res['ssim'] for res in results]
|
||||
|
||||
rmse_mean = np.mean(rmse_values)
|
||||
rmse_median = np.median(rmse_values)
|
||||
rmse_stdev = np.std(rmse_values, ddof=1)
|
||||
|
||||
ssim_mean = np.mean(ssim_values)
|
||||
ssim_median = np.median(ssim_values)
|
||||
ssim_stdev = np.std(ssim_values, ddof=1)
|
||||
|
||||
rmse_ci = calculate_confidence_interval(rmse_values)
|
||||
ssim_ci = calculate_confidence_interval(ssim_values)
|
||||
|
||||
print(f"\nRMSE - Mean: {rmse_mean}, Median: {rmse_median}, Std Dev: {rmse_stdev}, 95% CI: {rmse_ci}")
|
||||
print(f"SSIM - Mean: {ssim_mean}, Median: {ssim_median}, Std Dev: {ssim_stdev}, 95% CI: {ssim_ci}")
|
||||
|
||||
print(f"RMSE: {rmse_mean} ± {rmse_ci[1] - rmse_mean}")
|
||||
print(f"SSIM: {ssim_mean} ± {ssim_ci[1] - ssim_mean}")
|
||||
|
||||
def save_average_diff_map(results, save_path='average_diff_map.png'):
|
||||
if not results:
|
||||
print("No results available to create an average diff map.")
|
||||
return
|
||||
|
||||
avg_diff_map = None
|
||||
|
||||
for res in results:
|
||||
if avg_diff_map is None:
|
||||
avg_diff_map = np.zeros_like(res['diff_mask'])
|
||||
|
||||
avg_diff_map += res['diff_mask']
|
||||
|
||||
avg_diff_map /= len(results)
|
||||
|
||||
avg_diff_map = (avg_diff_map * 255).astype(np.uint8)
|
||||
|
||||
cv2.imwrite(save_path, avg_diff_map)
|
||||
|
||||
# Usage
|
||||
save_average_diff_map(results)
|
||||
@@ -1,4 +0,0 @@
|
||||
success = 10
|
||||
total = 10
|
||||
|
||||
print(success / total)
|
||||
@@ -1,48 +0,0 @@
|
||||
import csv
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def check_sleep(duration, sleep_function):
|
||||
start = time.perf_counter()
|
||||
sleep_function(duration)
|
||||
end = time.perf_counter()
|
||||
elapsed = end - start
|
||||
return abs(elapsed - duration)
|
||||
|
||||
def busy_sleep(duration):
|
||||
end_time = time.perf_counter() + duration
|
||||
while time.perf_counter() < end_time:
|
||||
pass
|
||||
|
||||
def measure_accuracy(sleep_function, durations, iterations=100):
|
||||
average_errors = []
|
||||
for duration in tqdm(durations):
|
||||
errors = [check_sleep(duration, sleep_function) for _ in range(iterations)]
|
||||
average_error = np.mean(errors)
|
||||
average_errors.append(average_error)
|
||||
return average_errors
|
||||
|
||||
durations = np.arange(0.001, 0.101, 0.001) # From 1ms to 100ms in 1ms increments
|
||||
iterations = 100
|
||||
|
||||
sleep_errors = measure_accuracy(time.sleep, durations, iterations)
|
||||
busy_sleep_errors = measure_accuracy(busy_sleep, durations, iterations)
|
||||
|
||||
def save_to_csv(filename, durations, sleep_errors, busy_sleep_errors):
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
fieldnames = ['duration', 'sleep_error', 'busy_sleep_error']
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
|
||||
writer.writeheader()
|
||||
for duration, sleep_error, busy_sleep_error in zip(durations, sleep_errors, busy_sleep_errors):
|
||||
writer.writerow({
|
||||
'duration': duration,
|
||||
'sleep_error': sleep_error,
|
||||
'busy_sleep_error': busy_sleep_error
|
||||
})
|
||||
print("Data saved to", filename)
|
||||
|
||||
save_to_csv('sleep_data.csv', durations * 1000, np.array(sleep_errors) * 1000, np.array(busy_sleep_errors) * 1000)
|
||||
@@ -1,33 +0,0 @@
|
||||
import csv
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def plot_from_csv(filename, save_plot=False):
|
||||
durations = []
|
||||
sleep_errors = []
|
||||
busy_sleep_errors = []
|
||||
|
||||
with open(filename, 'r') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
durations.append(float(row['duration']))
|
||||
sleep_errors.append(float(row['sleep_error']))
|
||||
busy_sleep_errors.append(float(row['busy_sleep_error']))
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.plot(durations, sleep_errors, label='time.sleep()', marker='o')
|
||||
plt.plot(durations, busy_sleep_errors, label='busy_sleep()', marker='x')
|
||||
plt.xlabel('Desired Delay (ms)')
|
||||
plt.ylabel('Average Error (ms)')
|
||||
plt.title('Sleep Accuracy: time.sleep() vs Busy-Wait Loop (macOS)')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
if save_plot:
|
||||
plt.savefig('sleep_accuracy_plot.png', dpi=300)
|
||||
print("Plot saved as sleep_accuracy_plot.png")
|
||||
|
||||
plt.show()
|
||||
|
||||
plot_from_csv('sleep_data.csv', save_plot=True)
|
||||
@@ -1,110 +0,0 @@
|
||||
import glob
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.stats as stats
|
||||
import seaborn as sns
|
||||
|
||||
# use this: https://www.estopwatch.net/
|
||||
|
||||
def read_file(file_path):
|
||||
df = pd.read_csv(file_path)
|
||||
df['Elapsed time'] = pd.to_datetime(df['Elapsed time'], errors='coerce')
|
||||
return df
|
||||
|
||||
|
||||
def analyze_new_error(run_df, groundtruth_df):
|
||||
cumulative_errors = run_df['Elapsed time'] - groundtruth_df['Elapsed time']
|
||||
cumulative_errors_in_seconds = cumulative_errors.dt.total_seconds()
|
||||
|
||||
new_errors_in_seconds = cumulative_errors_in_seconds.diff().fillna(cumulative_errors_in_seconds[0])
|
||||
new_error_points = new_errors_in_seconds[new_errors_in_seconds != 0].index.tolist()
|
||||
|
||||
return new_errors_in_seconds[new_error_points]
|
||||
|
||||
def calculate_statistics(errors):
|
||||
if len(errors) == 0:
|
||||
return {
|
||||
'mean_error': 0,
|
||||
'median_error': 0,
|
||||
'stddev_error': 0,
|
||||
'rmse_error': 0,
|
||||
'confidence_interval': (0, 0),
|
||||
'error_frequency': 0
|
||||
}
|
||||
|
||||
mean_error = np.mean(errors)
|
||||
median_error = np.median(errors)
|
||||
stddev_error = np.std(errors)
|
||||
rmse_error = np.sqrt(np.mean(np.square(errors)))
|
||||
|
||||
ci_low, ci_high = stats.t.interval(
|
||||
confidence=0.95,
|
||||
df=len(errors) - 1,
|
||||
loc=mean_error,
|
||||
scale=stats.sem(errors) if len(errors) > 1 else 0
|
||||
)
|
||||
|
||||
return {
|
||||
'mean_error': mean_error,
|
||||
'median_error': median_error,
|
||||
'stddev_error': stddev_error,
|
||||
'rmse_error': rmse_error,
|
||||
'confidence_interval': (ci_low, ci_high),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
groundtruth_file = 'groundtruth.csv'
|
||||
run_files = glob.glob('runs/*.csv')
|
||||
|
||||
groundtruth_df = read_file(groundtruth_file)
|
||||
run_dfs = {f'run{i+1}': read_file(file) for i, file in enumerate(run_files)}
|
||||
|
||||
total_errors = []
|
||||
total_points = 0
|
||||
all_errors = []
|
||||
|
||||
for run, df in run_dfs.items():
|
||||
errors = analyze_new_error(df, groundtruth_df)
|
||||
total_errors.extend(errors)
|
||||
all_errors.extend(errors)
|
||||
total_points += len(df)
|
||||
|
||||
results = calculate_statistics(errors)
|
||||
error_frequency = len(errors) / len(df)
|
||||
|
||||
print(f"Results for {run}:")
|
||||
print(f"Mean New Error: {results['mean_error']:.5f} seconds")
|
||||
print(f"Median New Error: {results['median_error']:.5f} seconds")
|
||||
print(f"Standard Deviation of New Error: {results['stddev_error']:.5f} seconds")
|
||||
print(f"RMSE of New Error: {results['rmse_error']:.5f} seconds")
|
||||
print(f"95% Confidence Interval of New Error: ({results['confidence_interval'][0]:.5f}, {results['confidence_interval'][1]:.5f}) seconds")
|
||||
print(f"New Error Frequency: {error_frequency*100:.5f} %")
|
||||
print('-----------------------------------------')
|
||||
|
||||
total_results = calculate_statistics(total_errors)
|
||||
total_error_frequency = len(total_errors) / total_points
|
||||
|
||||
print("Total Statistics:")
|
||||
print(f"Mean New Error: {total_results['mean_error']:.5f} seconds")
|
||||
print(f"Median New Error: {total_results['median_error']:.5f} seconds")
|
||||
print(f"Standard Deviation of New Error: {total_results['stddev_error']:.5f} seconds")
|
||||
print(f"RMSE of New Error: {total_results['rmse_error']:.5f} seconds")
|
||||
print(f"95% Confidence Interval of New Error: ({total_results['confidence_interval'][0]:.5f}, {total_results['confidence_interval'][1]:.5f}) seconds")
|
||||
print(f"New Error Frequency: {total_error_frequency*100:.5f} %")
|
||||
|
||||
# do plus minus
|
||||
print(f"New Error: {total_results['mean_error']:.5f} ± {total_results['confidence_interval'][1] - total_results['mean_error']:.5f} seconds")
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
sns.histplot(all_errors, bins=12, kde=False)
|
||||
plt.title('Distribution of Newly Introduced Errors (macOS)')
|
||||
plt.xlabel('Error Duration (seconds)')
|
||||
plt.ylabel('Frequency')
|
||||
plt.savefig('error_dist', dpi=300)
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,39 +0,0 @@
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
from ducktrack import MainInterface
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
interface = MainInterface(app)
|
||||
interface.show()
|
||||
|
||||
# TODO: come up with a better error solution to this
|
||||
|
||||
original_excepthook = sys.excepthook
|
||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||
print("Exception type:", exc_type)
|
||||
print("Exception value:", exc_value)
|
||||
|
||||
trace_details = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
trace_string = "".join(trace_details)
|
||||
|
||||
print("Exception traceback:", trace_string)
|
||||
|
||||
message = f"An error occurred!\n\n{exc_value}\n\n{trace_string}"
|
||||
interface.display_error_message(message)
|
||||
|
||||
original_excepthook(exc_type, exc_value, exc_traceback)
|
||||
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 836 KiB |
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,9 +0,0 @@
|
||||
git+https://github.com/moses-palmer/pynput.git@refs/pull/541/head # to make sure that it works on Apple Silicon
|
||||
pyautogui
|
||||
obsws-python
|
||||
PyQt6
|
||||
Pillow
|
||||
screeninfo
|
||||
wmi
|
||||
psutil
|
||||
pyinstaller
|
||||
@@ -1,3 +0,0 @@
|
||||
# Resource Collection
|
||||
|
||||
Manually gain some insights, then scale with careful code.
|
||||
@@ -1,28 +0,0 @@
|
||||
import praw
|
||||
|
||||
def search_reddit(keyword, subreddit, client_id, client_secret, user_agent, limit=10000):
|
||||
# Initialize PRAW with your Reddit application credentials
|
||||
reddit = praw.Reddit(client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user_agent=user_agent)
|
||||
|
||||
# Search the specified subreddit for the keyword
|
||||
results = reddit.subreddit(subreddit).search(keyword, limit=limit)
|
||||
|
||||
for post in results:
|
||||
print(f"Title: {post.title}")
|
||||
print(f"URL: {post.url}")
|
||||
print(f"Score: {post.score}")
|
||||
print(f"Comments: {post.num_comments}")
|
||||
print("------------------------")
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
CLIENT_ID = 'YOUR_CLIENT_ID'
|
||||
CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
|
||||
USER_AGENT = 'my_reddit_scraper'
|
||||
|
||||
keyword = "how to"
|
||||
subreddit = "vscode"
|
||||
|
||||
search_reddit(keyword, subreddit, CLIENT_ID, CLIENT_SECRET, USER_AGENT)
|
||||
@@ -1,268 +0,0 @@
|
||||
id,Source,InvolvedApp
|
||||
94d95f96-9699-4208-98ba-3c3119edf9c2,https://help.ubuntu.com/lts/ubuntu-help/addremove-install.html.en,OS
|
||||
bedcedc4-4d72-425e-ad62-21960b11fe0d,https://www.youtube.com/watch?v=D4WyNjt_hbQ&t=2s,OS
|
||||
43c2d64c-bab5-4dcb-a30c-b888321c319a,https://ubuntu.com/tutorials/command-line-for-beginners#4-creating-folders-and-files,OS
|
||||
7688b85f-87a4-4e4a-b2f8-f3d6c3f29b82,https://ubuntu.com/tutorials/command-line-for-beginners#5-moving-and-manipulating-files,OS
|
||||
ec4e3f68-9ea4-4c18-a5c9-69f89d1178b3,https://www.youtube.com/watch?v=D4WyNjt_hbQ&t=2s,OS
|
||||
a462a795-fdc7-4b23-b689-e8b6df786b78,https://help.ubuntu.com/lts/ubuntu-help/shell-exit.html.en,OS
|
||||
f9be0997-4b7c-45c5-b05c-4612b44a6118,https://help.ubuntu.com/lts/ubuntu-help/shell-notifications.html.en,OS
|
||||
ae039631-2b12-4637-84f6-c67d51511be3,https://help.ubuntu.com/lts/ubuntu-help/net-default-browser.html.en,OS
|
||||
e2eb4bf1-aa93-4192-b55d-03e2fb6dfd15,https://help.ubuntu.com/lts/ubuntu-help/contacts-add-remove.html.en,OS
|
||||
28cc3b7e-b194-4bc9-8353-d04c0f4d56d2,https://help.ubuntu.com/lts/ubuntu-help/sound-volume.html.en,OS
|
||||
5ea617a3-0e86-4ba6-aab2-dac9aa2e8d57,https://help.ubuntu.com/lts/ubuntu-help/files-recover.html.en,OS
|
||||
e0df059f-28a6-4169-924f-b9623e7184cc,https://help.ubuntu.com/lts/ubuntu-help/files-rename.html.en,OS
|
||||
ddc75b62-7311-4af8-bfb3-859558542b36,https://help.ubuntu.com/lts/ubuntu-help/addremove-remove.html.en,OS
|
||||
5c433d22-ed9a-4e31-91f5-54cf3e8acd63,https://help.ubuntu.com/lts/ubuntu-help/session-language.html.zh-CN,OS
|
||||
b6781586-6346-41cd-935a-a6b1487918fc,https://help.ubuntu.com/lts/ubuntu-help/clock-timezone.html.en,OS
|
||||
b3d4a89c-53f2-4d6b-8b6a-541fb5d205fa,https://help.ubuntu.com/lts/ubuntu-help/bluetooth-turn-on-off.html.en,OS
|
||||
3ce045a0-877b-42aa-8d2c-b4a863336ab8,https://help.ubuntu.com/lts/ubuntu-help/a11y-font-size.html.en,OS
|
||||
fe41f596-a71b-4c2f-9b2f-9dcd40b568c3,https://help.ubuntu.com/lts/ubuntu-help/power-percentage.html.en,OS
|
||||
a4d98375-215b-4a4d-aee9-3d4370fccc41,https://help.ubuntu.com/lts/ubuntu-help/privacy-screen-lock.html.en,OS
|
||||
765d2b74-88a7-4d50-bf51-34e4106fd24a,https://help.ubuntu.com/lts/ubuntu-help/files-delete.html.en,OS
|
||||
cc9d4f34-1ca0-4a1b-8ff2-09302696acb9,https://superuser.com/questions/178587/how-do-i-detach-a-process-from-terminal-entirely,OS
|
||||
5812b315-e7bd-4265-b51f-863c02174c28,https://superuser.com/questions/149404/create-an-ssh-user-who-only-has-permission-to-access-specific-folders,OS
|
||||
c56de254-a3ec-414e-81a6-83d2ce8c41fa,https://superuser.com/questions/28426/how-to-extract-text-with-ocr-from-a-pdf-on-linux,OS
|
||||
6ebbfb01-ea72-4226-a2a6-dc428e111ed2,https://superuser.com/questions/46748/how-do-i-make-bash-my-default-shell-on-ubuntu,OS
|
||||
4d2b519e-e872-4100-8ea3-fe71ab0f9133,https://stackoverflow.com/questions/11530090/adding-a-new-entry-to-the-path-variable-in-zsh,OS
|
||||
c288e301-e626-4b98-a1ab-159dcb162af5,https://stackoverflow.com/questions/41986507/unable-to-set-default-python-version-to-python3-in-ubuntu,OS
|
||||
13584542-872b-42d8-b299-866967b5c3ef,https://superuser.com/questions/72176/linux-set-default-terminal-size-and-screen-position,OS
|
||||
23393935-50c7-4a86-aeea-2b78fd089c5c,https://superuser.com/questions/91307/copying-only-jpg-from-a-directory-structure-to-another-location-linux,OS
|
||||
f10b16e1-c160-4cb3-989f-7b2ec89bc073,https://www.wikihow.com/Install-Gnome-on-Ubuntu,OS
|
||||
eb03d19a-b88d-4de4-8a64-ca0ac66f426b,https://www.youtube.com/shorts/t9JLUaT55UQ,MS Excel
|
||||
0bf05a7d-b28b-44d2-955a-50b41e24012a,https://www.youtube.com/shorts/FPAQaDTS8VY,MS Excel
|
||||
7b802dad-6e0f-4204-9815-d4e3f57627d8,https://www.youtube.com/shorts/Of-lzeP1usE,MS Excel
|
||||
7a4e4bc8-922c-4c84-865c-25ba34136be1,https://www.youtube.com/shorts/bvUhr1AHs44,MS Excel
|
||||
2bd59342-0664-4ccb-ba87-79379096cc08,https://www.youtube.com/shorts/L3Z-F1QTQFY,MS Excel
|
||||
a9f325aa-8c05-4e4f-8341-9e4358565f4f,https://www.youtube.com/shorts/A0gmEBRKXWs,MS Excel
|
||||
ecb0df7a-4e8d-4a03-b162-053391d3afaf,https://www.youtube.com/shorts/tXOovKn0H68,MS Excel
|
||||
7efeb4b1-3d19-4762-b163-63328d66303b,https://www.youtube.com/shorts/4jzXfZNhfmk,MS Excel
|
||||
4e6fcf72-daf3-439f-a232-c434ce416af6,https://www.youtube.com/shorts/0uxJccNCKcE,MS Excel
|
||||
6054afcb-5bab-4702-90a0-b259b5d3217c,https://www.youtube.com/shorts/JTbZ8sRxkdU,MS Excel
|
||||
abed40dc-063f-4598-8ba5-9fe749c0615d,https://www.youtube.com/shorts/xgf4ZpsEx5M,MS Excel
|
||||
01b269ae-2111-4a07-81fd-3fcd711993b0,https://www.youtube.com/shorts/VrUzPTIwQ04,MS Excel
|
||||
8b1ce5f2-59d2-4dcc-b0b0-666a714b9a14,https://www.youtube.com/shorts/Hbcwu6IQ1ns,MS Excel
|
||||
af2b02f7-acee-4be4-8b66-499fab394915,https://www.youtube.com/shorts/AwKsb5VmtBI,MS Excel
|
||||
da1d63b8-fa12-417b-ba18-f748e5f770f3,https://www.youtube.com/shorts/hquscnbz2-U,MS Excel
|
||||
636380ea-d5f6-4474-b6ca-b2ed578a20f1,https://www.youtube.com/shorts/_BYL6VOHLGw,"MS Excel, Edge"
|
||||
5ba77536-05c5-4aae-a9ff-6e298d094c3e,https://www.youtube.com/shorts/CuBC1evUS5I,MS Excel
|
||||
4bc4eaf4-ca5e-4db2-8138-8d4e65af7c0b,https://www.youtube.com/shorts/1adQWfjN-tI,MS Excel
|
||||
672a1b02-c62f-4ae2-acf0-37f5fb3052b0,https://www.youtube.com/shorts/2rhdQXI4Lng,MS Excel
|
||||
648fe544-16ba-44af-a587-12ccbe280ea6,https://www.youtube.com/shorts/sOPBMWaC6Uc,MS Excel
|
||||
8985d1e4-5b99-4711-add4-88949ebb2308,https://www.youtube.com/shorts/J5ts2Acv9Pc,MS Excel
|
||||
9e606842-2e27-43bf-b1d1-b43289c9589b,https://www.youtube.com/shorts/B-mGYDFOyUs,MS Excel
|
||||
fcb6e45b-25c4-4087-9483-03d714f473a9,https://www.youtube.com/shorts/GZipp7nOZS0,MS Excel
|
||||
68c0c5b7-96f3-4e87-92a7-6c1b967fd2d2,https://www.youtube.com/shorts/JEH5TsK-cCk,"MS Excel, Edge"
|
||||
fff629ea-046e-4793-8eec-1a5a15c3eb35,https://www.youtube.com/shorts/8WybtCdUT6w,MS Excel
|
||||
5c9a206c-bb00-4fb6-bb46-ee675c187df5,https://www.youtube.com/shorts/VbQtMNnq9i4,MS Excel
|
||||
e975ae74-79bd-4672-8d1c-dc841a85781d,https://www.youtube.com/shorts/GjT7gGe5Sr8,MS Excel
|
||||
34a6938a-58da-4897-8639-9b90d6db5391,https://www.youtube.com/shorts/gW37x2TkzOY,MS Excel
|
||||
b5a22759-b4eb-4bf2-aeed-ad14e8615f19,https://www.youtube.com/shorts/3xLa-D0C7Ic,MS Excel
|
||||
2f9913a1-51ed-4db6-bfe0-7e1c95b3139e,https://www.youtube.com/shorts/dGLRcmfVO6Q,MS Excel
|
||||
2558031e-401d-4579-8e00-3ecf540fb492,https://www.mrexcel.com/board/threads/sales-for-the-first-6-weeks.1249213/,MS Excel
|
||||
39aa4e37-dc91-482e-99af-132a612d40f3,https://www.libreofficehelp.com/add-insert-delete-copy-move-rename-a-worksheet-in-libreoffice-calc/,LibreOffice Calc
|
||||
0cecd4f3-74de-457b-ba94-29ad6b5dafb6,https://www.libreofficehelp.com/add-insert-delete-copy-move-rename-a-worksheet-in-libreoffice-calc/,LibreOffice Calc
|
||||
4188d3a4-077d-46b7-9c86-23e1a036f6c1,https://www.libreofficehelp.com/freeze-unfreeze-rows-columns-ranges-calc/,LibreOffice Calc
|
||||
51b11269-2ca8-4b2a-9163-f21758420e78,https://www.reddit.com/r/LibreOfficeCalc/comments/186pcc6/how_to_arrange_numbers_in_a_column_from_minimum/,LibreOffice Calc
|
||||
7e429b8d-a3f0-4ed0-9b58-08957d00b127,https://medium.com/@divyangichaudhari17/how-to-use-vlookup-and-hlookup-in-libre-calc-3370698bb3ff,LibreOffice Calc
|
||||
f5a90742-3fa2-40fc-a564-f29b054e0337,https://superuser.com/questions/1236149/libreoffice-calc-how-to-apply-functions-to-columns,LibreOffice Calc
|
||||
22df9241-f8d7-4509-b7f1-37e501a823f7,https://superuser.com/questions/1767185/how-do-you-move-cells-in-libreoffice-calc,LibreOffice Calc
|
||||
1434ca3e-f9e3-4db8-9ca7-b4c653be7d17,https://www.wikihow.com/Remove-Duplicates-in-Open-Office-Calc,LibreOffice Calc
|
||||
347ef137-7eeb-4c80-a3bb-0951f26a8aff,https://www.youtube.com/watch?v=bgO40-CjYNY,LibreOffice Calc
|
||||
6e99a1ad-07d2-4b66-a1ce-ece6d99c20a5,https://www.youtube.com/watch?v=nl-nXjJurhQ,LibreOffice Calc
|
||||
3aaa4e37-dc91-482e-99af-132a612d40f3,https://www.quora.com/How-can-you-import-export-CSV-files-with-LibreOffice-Calc-or-OpenOffice,LibreOffice Calc
|
||||
0decd4f3-74de-457b-ba94-29ad6b5dafb6,https://justclickhere.co.uk/resources/checkboxes-tick-boxes-libreoffice-calc/,LibreOffice Calc
|
||||
37608790-6147-45d0-9f20-1137bb35703d,https://www.youtube.com/shorts/uzPo_CPCHH8,MS Excel
|
||||
f9584479-3d0d-4c79-affa-9ad7afdd8850,https://youtube.com/shorts/feldd-Pn48c?si=9xJiem2uAHm6Jshb,LibreOffice Calc
|
||||
d681960f-7bc3-4286-9913-a8812ba3261a,https://www.youtube.com/shorts/d7U1S_IsTVM,LibreOffice Calc
|
||||
f6a90742-3fa2-40fc-a564-f29b054e0337,https://www.excel-easy.com/examples/drop-down-list.html,LibreOffice Calc
|
||||
21df9241-f8d7-4509-b7f1-37e501a823f7,https://www.youtube.com/watch?v=p5C4V_AO1UU,LibreOffice Calc
|
||||
1334ca3e-f9e3-4db8-9ca7-b4c653be7d17,https://techcommunity.microsoft.com/t5/excel/excel-workbook-top-way-too-big-can-t-see-rows-and-columns/m-p/4014694,LibreOffice Calc
|
||||
357ef137-7eeb-4c80-a3bb-0951f26a8aff,https://www.reddit.com/r/excel/comments/17zny8u/calculating_total_amount_earned_from_total_hours/,LibreOffice Calc
|
||||
6f99a1ad-07d2-4b66-a1ce-ece6d99c20a5,https://techcommunity.microsoft.com/t5/excel/sumarize-the-sheetnames/m-p/4014716,LibreOffice Calc
|
||||
aa3a8974-2e85-438b-b29e-a64df44deb4b,https://www.quora.com/Libre-Office-Calc-How-do-I-resize-all-cells-in-a-sheet-to-make-them-fit-to-1-page-for-printing-and-exporting-as-PDF,LibreOffice Calc
|
||||
a01fbce3-2793-461f-ab86-43680ccbae25,https://superuser.com/questions/1250677/how-to-set-decimal-separator-in-libre-office-calc,LibreOffice Calc
|
||||
4f07fbe9-70de-4927-a4d5-bb28bc12c52c,https://superuser.com/questions/1081048/libreoffice-calc-how-to-pad-number-to-fixed-decimals-when-used-within-formula,LibreOffice Calc
|
||||
e3b1d5fa-ed00-4129-bda1-1452bd2b6772,https://www.reddit.com/r/libreoffice/comments/tel112/calc_how_to_calculate_sum_by_categories/,LibreOffice Calc
|
||||
ca6a9524-f8e9-4d2f-9364-ab0cad567739,https://www.reddit.com/r/libreoffice/comments/113gmyc/how_to_remove_certain_text_from_cells_in_calc/,LibreOffice Calc
|
||||
a455e8d0-930f-40d2-9575-5e8d2d222f58,https://superuser.com/questions/562944/quickly-fill-blank-cells-in-a-list-in-libreoffice-calc,LibreOffice Calc
|
||||
83ee22c6-7737-49ce-9b5a-138c3e92af04,https://superuser.com/questions/661102/currency-conversion-in-libreoffice-calc,LibreOffice Calc
|
||||
819f61c2-ec77-4d3f-9996-0838ae5aacc8,https://superuser.com/questions/381696/creating-a-column-of-working-days-in-libreoffice-calc,LibreOffice Calc
|
||||
69d577b3-004e-4bca-89b2-0d7c2f6049e3,https://superuser.com/questions/387106/libreoffice-calc-how-to-get-total-for-hhmmss-cells,LibreOffice Calc
|
||||
0a1bf4ca-d4ea-4618-baa5-6e8dc1b46d82,https://superuser.com/questions/571915/sum-up-to-n-highest-value-out-of-a-series,LibreOffice Calc
|
||||
ac9bb6cb-1888-43ab-81e4-a98a547918cd,https://superuser.com/questions/1674211/how-to-change-colour-of-slide-number-in-libre-office,LibreOffice Impress
|
||||
5d901039-a89c-4bfb-967b-bf66f4df075e,https://superuser.com/questions/986776/how-can-i-stretch-an-image-in-a-libreoffice-impress-presentation-to-fill-the-pag,LibreOffice Impress
|
||||
071d4ace-091a-4ec3-886e-f4be55ae375d,https://superuser.com/questions/706860/hide-slide-numbers-and-slide-footer-on-first-and-second-slide-in-libreoffice-imp?rq=1,LibreOffice Impress
|
||||
550ce7e7-747b-495f-b122-acdc4d0b8e54,"https://technical-tips.com/blog/software/text-in-libreoffice-strikethrough--6948#:~:text=To%20strikethrough%20Text%20in%20LibreOffice%201%20In%20your,effect%22%20can%20your%20additionally%2C%20for%20example%2C%20double%20underline.",LibreOffice Impress
|
||||
455d3c66-7dc6-4537-a39a-36d3e9119df7,"https://www.libreofficehelp.com/export-libreoffice-impress-slides-images/#:~:text=Exporting%20a%20single%20slide%20as.jpg%2C.png%2C%20etc%20image%20is,on%20the%20checkbox%20Selection.%20Provide%20jpg%20quality%20options.",LibreOffice Impress
|
||||
af23762e-2bfd-4a1d-aada-20fa8de9ce07,https://superuser.com/questions/1059080/how-to-make-a-summary-slide-in-impress-listing-the-titles-of-all-slides-autom,LibreOffice Impress
|
||||
c59742c0-4323-4b9d-8a02-723c251deaa0,https://www.reddit.com/r/libreoffice/comments/17lcdrp/audio_not_supported_in_libreoffice_impress/,LibreOffice Impress
|
||||
39478d4a-1049-456f-aa77-407811393add,https://www.reddit.com/r/libreoffice/comments/jul3o8/putting_cap_or_hat_or_carat_symbol_in_libre/,LibreOffice Impress
|
||||
c3ad4442-499f-4e58-bc4e-1a1417ea9b8c,http://maharajacollege.ac.in/material/Libreofficeimpresspdf.pdf,LibreOffice Impress
|
||||
ef9d12bd-bcee-4ba0-a40e-918400f43ddf,https://www.reddit.com/r/libreoffice/comments/18elh3y/i_closed_the_slide_pannel_on_the_left_and_idk_how/,LibreOffice Impress
|
||||
9ec204e4-f0a3-42f8-8458-b772a6797cab,https://www.tiktok.com/@lil.d1rt_/video/7247574148887629083,LibreOffice Impress
|
||||
0f84bef9-9790-432e-92b7-eece357603fb,https://stackoverflow.com/questions/29036788/how-to-disable-libreoffice-impress-to-use-multiple-display,LibreOffice Impress
|
||||
ce88f674-ab7a-43da-9201-468d38539e4a,https://justclickhere.co.uk/resources/change-slides-in-impress-to-portrait/,LibreOffice Impress
|
||||
f0a334af-f91b-4c03-b578-aac9bec2b543,https://www.libreofficehelp.com/insert-video-impress-presentation/#Inserting_a_Video_in_Impress,LibreOffice Impress
|
||||
3b27600c-3668-4abd-8f84-7bcdebbccbdb,https://www.libreofficehelp.com/change-slide-background-impress/#All_Slides,LibreOffice Impress
|
||||
a097acff-6266-4291-9fbd-137af7ecd439,https://www.youtube.com/watch?v=DDmEvjs4iBw,LibreOffice Impress
|
||||
21760ecb-8f62-40d2-8d85-0cee5725cb72,https://www.libreofficehelp.com/add-animations-transitions-libreoffice-impress-slides/,LibreOffice Impress
|
||||
3cc4f35d-fa2e-4555-afb9-741b7c062a74,https://documentation.libreoffice.org/assets/Uploads/Documentation/en/IG7.6/IG76-ImpressGuide.pdf,LibreOffice Impress
|
||||
6ada715d-3aae-4a32-a6a7-429b2e43fb93,https://www.quora.com/How-do-you-insert-images-into-a-LibreOffice-Writer-document,LibreOffice Writer
|
||||
ecc2413d-8a48-416e-a3a2-d30106ca36cb,https://www.quora.com/How-can-I-insert-a-blank-page-on-libreoffice,LibreOffice Writer
|
||||
0e47de2a-32e0-456c-a366-8c607ef7a9d2,https://ask.libreoffice.org/t/how-to-start-page-numbering-on-a-certain-page/39931/4,LibreOffice Writer
|
||||
4bcb1253-a636-4df4-8cb0-a35c04dfef31,https://www.libreofficehelp.com/save-export-writer-documents-in-pdf-epub-format/,LibreOffice Writer
|
||||
0810415c-bde4-4443-9047-d5f70165a697,https://www.youtube.com/watch?v=Q_AaL6ljudU,LibreOffice Writer
|
||||
e528b65e-1107-4b8c-8988-490e4fece599,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
66399b0d-8fda-4618-95c4-bfc6191617e9,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
936321ce-5236-426a-9a20-e0e3c5dc536f,https://www.youtube.com/watch?v=l25Evu4ohKg,LibreOffice Writer
|
||||
663876c7-3471-43db-ba51-f410b13d9d7d,https://askubuntu.com/questions/319593/how-to-type-science-equations-in-libre-office,LibreOffice Writer
|
||||
3ef2b351-8a84-4ff2-8724-d86eae9b842e,https://askubuntu.com/questions/1066351/how-do-you-center-align-in-libreoffice#:~:text=Ctrl%20%2B%20e%20will%20Center%20align%20the%20cursor%20for%20you.,LibreOffice Writer
|
||||
45d61a06-6545-4422-97b7-bc76cfa964c1,https://stackoverflow.com/questions/71685737/how-to-replace-all-newlines-with-paragraph-marks-in-libreoffice-write,LibreOffice Writer
|
||||
0b17a146-2934-46c7-8727-73ff6b6483e8,https://askubuntu.com/questions/245695/how-do-you-insert-subscripts-and-superscripts-into-ordinary-non-formula-text-i,LibreOffice Writer
|
||||
0e763496-b6bb-4508-a427-fad0b6c3e195,https://ask.libreoffice.org/t/how-do-i-change-the-font-for-the-whole-document-in-writer/9220,LibreOffice Writer
|
||||
f178a4a9-d090-4b56-bc4c-4b72a61a035d,https://ask.libreoffice.org/t/how-do-i-make-times-new-roman-the-default-font-in-lo/64604,LibreOffice Writer
|
||||
0a0faba3-5580-44df-965d-f562a99b291c,https://stackoverflow.com/questions/64528055/how-to-make-part-of-my-sentence-left-aligned-and-rest-as-right-aligned,LibreOffice Writer
|
||||
e246f6d8-78d7-44ac-b668-fcf47946cb50,https://ask.libreoffice.org/t/how-to-change-text-size-color-of-italic-font/77712,LibreOffice Writer
|
||||
8472fece-c7dd-4241-8d65-9b3cd1a0b568,https://stackoverflow.com/questions/37259827/libreoffice-writer-how-to-set-different-colors-to-each-letter,LibreOffice Writer
|
||||
88fe4b2d-3040-4c70-9a70-546a47764b48,https://stackoverflow.com/questions/56554555/libreoffice-writer-how-to-create-empty-line-space-after-every-period-in-a-par,LibreOffice Writer
|
||||
6a33f9b9-0a56-4844-9c3f-96ec3ffb3ba2,https://superuser.com/questions/762500/how-do-i-find-all-highlighted-text-in-libreoffice-writer,LibreOffice Writer
|
||||
d53ff5ee-3b1a-431e-b2be-30ed2673079b,https://ask.libreoffice.org/t/how-to-convert-all-uppercase-to-lowercase/53341,LibreOffice Writer
|
||||
72b810ef-4156-4d09-8f08-a0cf57e7cefe,https://superuser.com/questions/657792/libreoffice-writer-how-to-apply-strikethrough-text-formatting?rq=1,LibreOffice Writer
|
||||
6f81754e-285d-4ce0-b59e-af7edb02d108,https://superuser.com/questions/789473/remove-duplicate-lines-in-libreoffice-openoffice-writer,LibreOffice Writer
|
||||
41c621f7-3544-49e1-af8d-dafd0f834f75,https://superuser.com/questions/1668018/how-to-auto-format-lines-in-libre-office-writer,LibreOffice Writer
|
||||
b21acd93-60fd-4127-8a43-2f5178f4a830,https://superuser.com/questions/1097199/how-can-i-double-space-a-document-in-libreoffice?rq=1,LibreOffice Writer
|
||||
59f21cfb-0120-4326-b255-a5b827b38967,https://docs.videolan.me/vlc-user/desktop/3.0/en/basic/media.html#playing-a-file,VLC player
|
||||
8ba5ae7a-5ae5-4eab-9fcc-5dd4fe3abf89,https://docs.videolan.me/vlc-user/desktop/3.0/en/basic/recording/playing.html#choose-your-recordings-folder,VLC player
|
||||
8f080098-ddb1-424c-b438-4e96e5e4786e,https://medium.com/@jetscribe_ai/how-to-extract-mp3-audio-from-videos-using-vlc-media-player-beeef644ebfb,VLC player
|
||||
bba3381f-b5eb-4439-bd9e-80c22218d5a7,https://www.quora.com/How-do-I-play-online-videos-using-the-VLC-media-player,VLC player
|
||||
a1c3ab35-02de-4999-a7ed-2fd12c972c6e,https://www.quora.com/How-do-I-compress-a-video-with-VLC,VLC player
|
||||
fba2c100-79e8-42df-ae74-b592418d54f4,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
d70666e4-7348-42c7-a06a-664094c5df3c,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
efcf0d81-0835-4880-b2fd-d866e8bc2294,"https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s, https://help.ubuntu.com/stable/ubuntu-help/look-background.html.en",VLC player
|
||||
8d9fd4e2-6fdb-46b0-b9b9-02f06495c62f,https://www.youtube.com/watch?v=XHprwDJ0-fU&t=436s,VLC player
|
||||
aa4b5023-aef6-4ed9-bdc9-705f59ab9ad6,https://videoconverter.wondershare.com/vlc/how-to-rotate-a-video-using-vlc.html?gad_source=1&gclid=CjwKCAiA-vOsBhAAEiwAIWR0TaGSOLkYiBeVQGZSyfeUP3g-tIvYxffl5RFIu0-zrUL1IF41eCw1JRoCnCMQAvD_BwE,VLC player
|
||||
386dbd0e-0241-4a0a-b6a2-6704fba26b1c,https://superuser.com/questions/1708415/pause-and-play-vlc-in-background?rq=1,VLC player
|
||||
9195653c-f4aa-453d-aa95-787f6ccfaae9,https://superuser.com/questions/1513285/how-can-i-increase-the-maximum-volume-output-by-vlc?rq=1,VLC player
|
||||
5ac2891a-eacd-4954-b339-98abba077adb,"https://superuser.com/questions/1412810/how-to-prevent-vlc-media-player-from-auto-closing-after-video-end#:%7E:text=Click%20on%20%22Media%22on%20the,VLC%20player%20after%20video%20ending",VLC player
|
||||
0d95d28a-9587-433b-a805-1fbe5467d598,https://superuser.com/questions/1299036/vlc-how-to-open-the-folder-of-the-current-playing-video?noredirect=1&lq=1,VLC player
|
||||
d06f0d4d-2cd5-4ede-8de9-598629438c6e,https://superuser.com/questions/1039392/changing-colour-of-vlc-volume-slider,VLC player
|
||||
a5bbbcd5-b398-4c91-83d4-55e1e31bbb81,https://superuser.com/questions/776056/how-to-hide-bottom-toolbar-in-vlc,VLC player
|
||||
f3977615-2b45-4ac5-8bba-80c17dbe2a37,https://www.reddit.com/r/Fedora/comments/rhljzd/how_to_run_multiple_instances_of_vlc_media_player/,VLC player
|
||||
c669a35f-d45a-450e-b1f2-f473748337bb,https://www.quora.com/How-do-I-fast-forward-a-video-in-VLC-player,VLC player
|
||||
d1ba14d0-fef8-4026-8418-5b581dc68ca0,https://superuser.com/questions/306154/how-to-use-a-b-repeat-feature-of-vlc,VLC player
|
||||
215dfd39-f493-4bc3-a027-8a97d72c61bf,https://superuser.com/questions/1224784/how-to-change-vlcs-splash-screen,VLC player
|
||||
bb5e4c0d-f964-439c-97b6-bdb9747de3f4,https://www.wikihow.com/Remove-an-Email-Account-from-Thunderbird,ThunderBird
|
||||
7b6c7e24-c58a-49fc-a5bb-d57b80e5b4c3,https://www.wikihow.com/Access-Gmail-With-Mozilla-Thunderbird,ThunderBird
|
||||
b188fe10-ae67-4db8-a154-26a0b8ff8f1e,https://www.reddit.com/r/Thunderbird/comments/17vv2os/restore_readability_in_message_list_pane/,ThunderBird
|
||||
12086550-11c0-466b-b367-1d9e75b3910e,https://www.bitrecover.com/blog/manage-thunderbird-profiles/,ThunderBird
|
||||
06fe7178-4491-4589-810f-2e2bc9502122,https://www.quora.com/How-do-I-backup-email-files-in-Mozilla-Thunderbird,ThunderBird
|
||||
6766f2b8-8a72-417f-a9e5-56fcaa735837,"https://www.adsigner.com/user-manual/signatures/setup-email-client-thunderbird/#:~:text=is%20probably%20hidden.-,Right%20click%20on%20the%20empty%20space%20at%20the%20top%20of,signature%20from%20a%20file%20instead.",ThunderBird
|
||||
e1e75309-3ddb-4d09-92ec-de869c928143,https://support.mozilla.org/en-US/kb/organize-your-messages-using-filters,ThunderBird
|
||||
3d1682a7-0fb0-49ae-a4dc-a73afd2d06d5,https://support.mozilla.org/en-US/kb/organize-your-messages-using-filters,ThunderBird
|
||||
35253b65-1c19-4304-8aa4-6884b8218fc0,https://support.mozilla.org/en-US/questions/1259354,ThunderBird
|
||||
d088f539-cab4-4f9a-ac92-9999fc3a656e,https://support.mozilla.org/en-US/kb/how-use-attachments,ThunderBird
|
||||
2ad9387a-65d8-4e33-ad5b-7580065a27ca,"https://support.mozilla.org/bm/questions/1027435, https://www.wikihow.tech/Create-Folders-in-Mozilla-Thunderbird",ThunderBird
|
||||
480bcfea-d68f-4aaa-a0a9-2589ef319381,https://www.reddit.com/r/Thunderbird/comments/182dg5p/unified_inbox_howto/,ThunderBird
|
||||
37b9808f-b2b4-4177-ab00-9ddfae4bad27,https://www.quora.com/How-can-I-schedule-Mozilla-Thunderbird-to-turn-off-automatically,ThunderBird
|
||||
af630914-714e-4a24-a7bb-f9af687d3b91,https://stackoverflow.com/questions/11333148/adding-a-toolbar-button-to-a-thundebird-compose-message-window?rq=3,ThunderBird
|
||||
3299584d-8f11-4457-bf4c-ce98f7600250,https://superuser.com/questions/1643561/would-like-to-see-the-email-address-from-sender-in-the-column,ThunderBird
|
||||
030eeff7-b492-4218-b312-701ec99ee0cc,https://superuser.com/questions/1781004/how-do-i-remove-the-indentation-and-character-in-quoted-text-of-a-reply-mess,ThunderBird
|
||||
94760984-3ff5-41ee-8347-cf1af709fea0,https://superuser.com/questions/1757333/how-can-i-view-thunderbird-in-full-dark-mode,ThunderBird
|
||||
99146c54-4f37-4ab8-9327-5f3291665e1e,https://superuser.com/questions/1764409/how-to-send-email-with-thunderbird-without-configuring-an-incoming-email-service,ThunderBird
|
||||
9656a811-9b5b-4ddf-99c7-5117bcef0626,https://superuser.com/questions/205240/is-there-a-way-to-get-a-popup-confirmation-box-when-you-send-an-email-in-thunder?rq=1,ThunderBird
|
||||
c9e7eaf2-b1a1-4efc-a982-721972fa9f02,https://superuser.com/questions/544480/how-to-apply-automatic-message-filters-to-subfolders-too?noredirect=1&lq=1,ThunderBird
|
||||
bb5e4c0d-f964-439c-97b6-bdb9747de3f4,https://support.google.com/chrome/answer/95426?sjid=16867045591165135686-AP,Chrome
|
||||
7b6c7e24-c58a-49fc-a5bb-d57b80e5b4c3,https://support.google.com/chrome/answer/95647?hl=en&ref_topic=7438325&sjid=16867045591165135686-AP#zippy=%2Cdelete-cookies-from-a-site,Chrome
|
||||
12086550-11c0-466b-b367-1d9e75b3910e,https://www.quora.com/What-are-the-cool-tricks-to-use-Google-Chrome,Chrome
|
||||
06fe7178-4491-4589-810f-2e2bc9502122,https://www.wikihow.com/Switch-Tabs-in-Chrome,Chrome
|
||||
6766f2b8-8a72-417f-a9e5-56fcaa735837,https://support.google.com/chrome/thread/205881926/it-s-possible-to-load-unpacked-extension-automatically-in-chrome?hl=en,Chrome
|
||||
e1e75309-3ddb-4d09-92ec-de869c928143,https://in5stepstutorials.com/google-chrome/save-web-page-as-pdf-in-chrome.php,Chrome
|
||||
3d1682a7-0fb0-49ae-a4dc-a73afd2d06d5,https://in5stepstutorials.com/google-chrome/add-change-delete-autofill-address.php,Chrome
|
||||
35253b65-1c19-4304-8aa4-6884b8218fc0,"https://www.laptopmag.com/articles/how-to-create-desktop-shortcuts-for-web-pages-using-chrome, https://www.reddit.com/r/chrome/comments/13xcbap/crete_shortcut_option_missing/",Chrome
|
||||
d088f539-cab4-4f9a-ac92-9999fc3a656e,https://medium.com/@inkverseuk2/useful-tips-and-tricks-for-the-google-chrome-browser-ac7d0d24b3cc,Chrome
|
||||
2ad9387a-65d8-4e33-ad5b-7580065a27ca,https://www.youtube.com/watch?v=IN-Eq_UripQ,Chrome
|
||||
7a5a7856-f1b6-42a4-ade9-1ca81ca0f263,https://www.youtube.com/watch?v=ZaZ8GcTxjXA,Chrome
|
||||
3720f614-37fd-4d04-8a6b-76f54f8c222d,https://superuser.com/questions/984668/change-interface-language-of-chrome-to-english,Chrome
|
||||
b63059a2-53bc-4163-a89f-3ac948c74081,https://superuser.com/questions/1303418/how-do-i-make-chrome-block-absolutely-all-pop-ups?rq=1,Chrome
|
||||
44ee5668-ecd5-4366-a6ce-c1c9b8d4e938,https://superuser.com/questions/1787991/clear-browsing-history-from-specific-site-on-chrome,Chrome
|
||||
b5ebc8c6-6329-4373-85b4-9421c97375e9,https://superuser.com/questions/364470/is-there-a-way-to-view-google-chrome-browsing-history-past-three-months-ago?rq=1,Chrome
|
||||
93eabf48-6a27-4cb6-b963-7d5fe1e0d3a9,https://superuser.com/questions/1417973/how-to-disable-google-chrome-dark-mode,Chrome
|
||||
2ae9ba84-3a0d-4d4c-8338-3a1478dc5fe3,https://superuser.com/questions/1393683/how-to-change-the-username-in-google-chrome-profiles?rq=1,Chrome
|
||||
480bcfea-d68f-4aaa-a0a9-2589ef319381,https://bugartisan.medium.com/disable-the-new-chrome-ui-round-in-2023-db168271f71e,Chrome
|
||||
37b9808f-b2b4-4177-ab00-9ddfae4bad27,https://www.reddit.com/r/chrome/comments/17niw3h/tutorial_how_to_disable_the_download_bubble_in/,Chrome
|
||||
af630914-714e-4a24-a7bb-f9af687d3b91,https://www.howtogeek.com/680260/how-to-change-chromes-default-text-size/,Chrome
|
||||
ae78f875-5b98-4907-bbb5-9c737fc68c03,https://support.google.com/chrome/thread/219988391/increase-search-results-per-page?hl=en,Chrome
|
||||
0ed39f63-6049-43d4-ba4d-5fa2fe04a951,https://www.quora.com/How-do-you-find-and-replace-text-in-Visual-Studio-Code,VS Code
|
||||
b421106e-b282-4c41-af72-37c95493f95f,https://stackoverflow.com/questions/74153883/launch-vscode-with-new-txt-file,VS Code
|
||||
53ad5833-3455-407b-bbc6-45b4c79ab8fb,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
eabc805a-bfcf-4460-b250-ac92135819f6,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
3486f395-ad68-459c-8c39-ea07de934dd4,https://www.youtube.com/watch?v=VqCgcpAypFQ,VS Code
|
||||
982d12a5-beab-424f-8d38-d2a48429e511,https://www.youtube.com/watch?v=ORrELERGIHs,VS Code
|
||||
4e60007a-f5be-4bfc-9723-c39affa0a6d3,"https://campbell-muscle-lab.github.io/howtos_Python/pages/documentation/best_practices/vscode_docstring_extension/vscode_docstring_extension.html#:~:text=Type%2C%20Ctrl%20%2B%20Shift%20%2B%20P,select%20the%20NumPy%20docstring%20format.",VS Code
|
||||
e2b5e914-ffe1-44d2-8e92-58f8c5d92bb2,https://superuser.com/questions/1386061/how-to-suppress-some-python-errors-warnings-in-vs-code,VS Code
|
||||
9439a27b-18ae-42d8-9778-5f68f891805e,https://stackoverflow.com/questions/75832474/how-to-keep-cursor-in-debug-console-when-debugging-in-visual-studio-code,VS Code
|
||||
ae506c68-352c-4094-9caa-ee9d42052317,https://superuser.com/questions/1460404/get-visual-studio-code-terminal-history?rq=1,VS Code
|
||||
ea98c5d7-3cf9-4f9b-8ad3-366b58e0fcae,https://superuser.com/questions/1748097/vs-code-disable-tree-view-find-explorer-search,VS Code
|
||||
c714dcee-cad3-4e12-8f3c-12bdcfcdb048,https://superuser.com/questions/1417361/how-to-disable-file-filtering-in-vs-code-sidebar-explorer?rq=1,VS Code
|
||||
930fdb3b-11a8-46fe-9bac-577332e2640e,https://superuser.com/questions/1270103/how-to-switch-the-cursor-between-terminal-and-code-in-vscode,VS Code
|
||||
276cc624-87ea-4f08-ab93-f770e3790175,https://www.quora.com/unanswered/How-do-you-set-the-line-length-in-Visual-Studio-Code,VS Code
|
||||
9d425400-e9b2-4424-9a4b-d4c7abac4140,https://superuser.com/questions/1466771/is-there-a-way-to-make-editor-tabs-stack-in-vs-code,VS Code
|
||||
7a4deb26-d57d-4ea9-9a73-630f66a7b568,https://www.quora.com/How-do-I-edit-a-photo-in-GIMP,GIMP
|
||||
554785e9-4523-4e7a-b8e1-8016f565f56a,https://www.quora.com/How-do-I-edit-a-photo-in-GIMP,GIMP
|
||||
77b8ab4d-994f-43ac-8930-8ca087d7c4b4,https://superuser.com/questions/1636113/how-to-get-gimp-to-recognize-images-or-pictures-folder-as-the-default-folder-for,GIMP
|
||||
f4aec372-4fb0-4df5-a52b-79e0e2a5d6ce,https://superuser.com/questions/612338/how-do-i-select-and-move-an-object-in-gimp,GIMP
|
||||
d52d6308-ec58-42b7-a2c9-de80e4837b2b,https://superuser.com/questions/1447106/how-to-get-rid-of-the-gimp-tool-options-box,GIMP
|
||||
2a729ded-3296-423d-aec4-7dd55ed5fbb3,https://www.youtube.com/watch?v=lOzSiOIipSM,GIMP
|
||||
b148e375-fe0b-4bec-90e7-38632b0d73c2,https://www.quora.com/How-do-I-add-layers-in-GIMP,GIMP
|
||||
a746add2-cab0-4740-ac36-c3769d9bfb46,https://www.youtube.com/watch?v=_L_MMU22bAw,GIMP
|
||||
7b7617bd-57cc-468e-9c91-40c4ec2bcb3d,https://www.youtube.com/watch?v=G_PjQAy0iiU,GIMP
|
||||
d16c99dc-2a1e-46f2-b350-d97c86c85c15,https://stackoverflow.com/questions/75185543/use-gimp-to-resize-image-in-one-layer-only,GIMP
|
||||
573f79b5-abfe-4507-b455-251d45fe6198,https://stackoverflow.com/questions/45196895/gimp-add-padding-to-multiple-images,GIMP
|
||||
06ca5602-62ca-47f6-ad4f-da151cde54cc,https://stackoverflow.com/questions/74664666/how-to-export-palette-based-png-in-gimp,GIMP
|
||||
fa9b1e10-4d2d-4a13-af76-7efa822b6a8b,https://stackoverflow.com/questions/24626608/how-to-combine-several-png-images-as-layers-in-a-single-xcf-image,GIMP
|
||||
6b2b72ed-3a10-4849-876a-750f7cdf3886,https://stackoverflow.com/questions/21018007/resize-image-to-fit-canvas-gimp,GIMP
|
||||
d0e42fd2-d290-46b3-b598-a6e2b7be9c85,https://stackoverflow.com/questions/56758689/stop-gimp-from-merging-layers-when-de-selecting,GIMP
|
||||
e2dd0213-26db-4349-abe5-d5667bfd725c,https://superuser.com/questions/839650/how-to-move-an-inserted-text-box-in-gimp,GIMP
|
||||
f723c744-e62c-4ae6-98d1-750d3cd7d79d,https://www.reddit.com/r/GIMP/comments/12e57w8/how_to_use_gimp_to_exaggerate_contrast/,GIMP
|
||||
8d6b1c9c-1aab-47fe-9ba5-e84c838d0c57,https://www.quora.com/How-can-email-attachments-be-converted-into-a-word-document-using-Mozilla-Thunderbird,multiple
|
||||
11e1e614-9696-4d94-88c9-8e556880d41a,https://ifttt.com/applets/L2A89geP-send-chrome-software-update-release-alerts-to-email,multiple
|
||||
57956154-f0fe-486b-88b8-e7126da035a9,https://zapier.com/apps/email/integrations/google-sheets/547/get-email-notifications-for-new-rows-in-a-google-sheets-spreadsheet,multiple
|
||||
ec14c524-b245-456d-abd6-ec12c746e9f8,https://zapier.com/apps/gmail/integrations/google-sheets/2618/save-new-gmail-emails-matching-certain-traits-to-a-google-spreadsheet,multiple
|
||||
cbf5fbda-425e-4619-bcf2-0ea8d4c0bfa3,https://zapier.com/apps/google-sheets/integrations/google-slides/13919/refresh-charts-on-a-google-slides-presentation-when-rows-are-updated-on-google-sheets,multiple
|
||||
a54284d0-7b93-4327-bfcc-3a421516dbdd,https://superuser.com/questions/655622/cannot-drag-images-from-thunderbird-to-word,multiple
|
||||
58565672-7bfe-48ab-b828-db349231de6b,https://superuser.com/questions/1792660/open-link-from-other-application-does-not-open-the-url-in-firefox,multiple
|
||||
6d72aad6-187a-4392-a4c4-ed87269c51cf,https://superuser.com/questions/923171/converting-openoffice-impress-presentation-to-video-without-screen-recording,multiple
|
||||
937087b6-f668-4ba6-9110-60682ee33441,https://superuser.com/questions/187440/set-default-ubuntu-video-player-as-vlc,multiple
|
||||
f8cfa149-d1c1-4215-8dac-4a0932bad3c2,https://superuser.com/questions/1803088/libreoffice-calc-clears-clipboard,multiple
|
||||
5e974913-6905-4c3f-8b65-d7837f3931cc,https://stackoverflow.com/questions/61856141/how-can-i-start-thunderbird-and-minimize-the-window-on-startup-in-ubuntu,multiple
|
||||
7c179dad-f1c7-4892-b53f-d1c4023d23c7,https://stackoverflow.com/questions/21155085/pasting-excel-tables-in-thunderbird-e-mail-client,multiple
|
||||
4a68b2dd-70f2-4532-9bc1-d21878bd8cb2,https://stackoverflow.com/questions/65669955/thunderbird-how-to-send-a-mail-to-all-receivers-of-a-folder,multiple
|
||||
c8457fde-b14b-4aba-b402-144842ea29e1,https://stackoverflow.com/questions/65788200/how-to-open-xlsx-files-in-ms-excel-from-vs-code,multiple
|
||||
81c425f5-78f3-4771-afd6-3d2973825947,https://www.zyxware.com/articles/3770/how-to-transfer-data-in-libreoffice-calc-to-libreoffice-writer-in-table-format,multiple
|
||||
bb83cab4-e5c7-42c7-a67b-e46068032b86,https://ask.libreoffice.org/t/save-impress-presentation-as-writer-document/5291/4,multiple
|
||||
227d2f97-562b-4ccb-ae47-a5ec9e142fbb,https://discourse.gnome.org/t/gimp-and-libre-office-writer/15430/4,multiple
|
||||
a6bbc08c-51e9-4ee4-9327-83d05075d960,https://forum.openoffice.org/en/forum/viewtopic.php?t=105055,multiple
|
||||
964e6e03-ba31-466b-8c15-5a351a81f675,https://www.maketecheasier.com/mail-merge-thunderbird-calc/,multiple
|
||||
2fe4b718-3bd7-46ec-bdce-b184f5653624,https://www.thewindowsclub.com/how-to-create-animated-gif-from-a-video-file-using-vlc-and-gimp,multiple
|
||||
d02b9364-6bb0-4c7e-9dbd-4db62822bc26,https://stackoverflow.com/questions/38306910/simple-python-script-to-get-a-libreoffice-base-field-and-play-on-vlc,multiple
|
||||
57fb469b-127a-46fa-8281-bbb3840efdf5,https://support.mozilla.org/en-US/questions/1150626,multiple
|
||||
3680a5ee-6870-426a-a997-eba929a0d25c,https://unix.stackexchange.com/questions/510850/how-to-open-calc-from-terminal-and-insert-files,multiple
|
||||
2d8c8a20-6f54-4c2e-ad56-61fbe7af6b78,https://www.quora.com/How-do-I-force-LibreOffice-Calc-to-recalculate-a-spreadsheet-from-the-command-line,multiple
|
||||
ee9a3c83-f437-4879-8918-be5efbb9fac7,https://stackoverflow.com/questions/64589140/convert-ods-to-csv-using-command-line-when-libreoffice-instance-is-running,multiple
|
||||
f7dfbef3-7697-431c-883a-db8583a4e4f9,https://www.thegeekdiary.com/libreoffice-command-examples-in-linux/,multiple
|
||||
2b9493d7-49b8-493a-a71b-56cd1f4d6908,https://devicetests.com/kill-libreoffice-writer-command-line-ubuntu,multiple
|
||||
51f5801c-18b3-4f25-b0c3-02f85507a078,https://github.com/danielrcollins1/ImpressExtractNotes,multiple
|
||||
81de345e-5473-4cb6-a74d-b6abf3475a6a,https://stackoverflow.com/questions/45588952/how-can-i-compose-and-send-email-in-thunderbird-from-commandline,multiple
|
||||
2c9fc0de-3ee7-45e1-a5df-c86206ad78b5,https://nikki-ricks.medium.com/how-to-use-git-add-commit-and-push-in-vs-code-and-command-line-35c0e8c47b62,multiple
|
||||
510f64c8-9bcc-4be1-8d30-638705850618,https://www.geeksforgeeks.org/how-to-start-vs-code-from-the-terminal-command-line/,multiple
|
||||
9ff484f7-5c09-4398-ae29-d5904e59e138,https://stackoverflow.com/questions/38606973/playing-opening-and-pausing-vlc-command-line-executed-from-python-scripts,multiple
|
||||
d9b7c649-c975-4f53-88f5-940b29c47247,https://marketplace.uipath.com/listings/extract-the-first-1000-gmail-emails-from-the-current-month-in-a-new-google-sheets-report,multiple
|
||||
be4ef0dc-0f70-4936-81d8-3cd2b04482f8,https://marketplace.uipath.com/listings/table-data-extraction-for-sales-opportunities-to-excel-workbook,multiple
|
||||
78aed49a-a710-4321-a793-b611a7c5b56b,https://marketplace.uipath.com/listings/upload-email-attachments-from-gmail-to-google-drive,multiple
|
||||
897e3b53-5d4d-444b-85cb-2cdc8a97d903,https://marketplace.uipath.com/listings/convert-word-file-to-pdf-and-store-in-onedrive,multiple
|
||||
4e9f0faf-2ecc-4ae8-a804-28c9a75d1ddc,https://marketplace.uipath.com/listings/extract-data-from-a-new-invoice-file-in-google-drive-and-store-it-in-google-sheets4473,multiple
|
||||
b52b40a5-ad70-4c53-b5b0-5650a8387052,https://marketplace.uipath.com/listings/merge-pdfs-from-gmail-email-attachments-and-upload-to-gogle-drive,multiple
|
||||
46407397-a7d5-4c6b-92c6-dbe038b1457b,https://marketplace.uipath.com/listings/upload-to-google-drive-images-from-pdf-attachments-received-via-gmail,multiple
|
||||
a0b9dc9c-fc07-4a88-8c5d-5e3ecad91bcb,https://marketplace.uipath.com/listings/backup-important-emails-to-onedrive-or-sharepoint,multiple
|
||||
665f4af1-617d-4009-baff-84ff66071e6a,https://www.howtogeek.com/663927/how-to-open-google-chrome-using-command-prompt-on-windows-10/#open-chrome-straight-to-a-specific-website,multiple
|
||||
e6313b30-3903-4ed9-8c7d-4c47bf51fc96,https://stackoverflow.com/questions/12258086/how-do-i-run-google-chrome-as-root,multiple
|
||||
|
@@ -1,238 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
import yt_dlp
|
||||
from docx import Document
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
from io import BytesIO
|
||||
from docx import Document
|
||||
import re
|
||||
import markdownify
|
||||
from markdownify import markdownify as md
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
codepoint = ord(c)
|
||||
# conditions ordered by presumed frequency
|
||||
return (
|
||||
0x20 <= codepoint <= 0xD7FF or
|
||||
codepoint in (0x9, 0xA, 0xD) or
|
||||
0xE000 <= codepoint <= 0xFFFD or
|
||||
0x10000 <= codepoint <= 0x10FFFF
|
||||
)
|
||||
|
||||
def download_and_clean_youtube_subtitles(video_url, txt_filepath):
|
||||
# set up youtube-dl options to download the subtitles
|
||||
subtitles_path = txt_filepath[0:-4]
|
||||
ydl_opts = {
|
||||
'skip_download': True,
|
||||
'writesubtitles': True,
|
||||
'writeautomaticsub': True, # if no subtitles are available, try to generate them
|
||||
'subtitleslangs': ['en'],
|
||||
'outtmpl': f'{subtitles_path}.%(ext)s',
|
||||
'quiet': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# download the subtitles
|
||||
ydl.download([video_url])
|
||||
subtitle_file = f'{subtitles_path}.en.vtt'
|
||||
|
||||
# read the subtitle file
|
||||
subtitles = []
|
||||
try:
|
||||
with open(subtitle_file, 'r', encoding='utf-8') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
# define a pattern to match the time line
|
||||
pattern = re.compile(r'(\d{2}:\d{2}:\d{2}.\d{3} --> \d{2}:\d{2}:\d{2}.\d{3})|(^WEBVTT)|(^Kind: captions)|(^Language: .*)')
|
||||
|
||||
# clean the subtitles
|
||||
for line in lines:
|
||||
# if this line is a time line or it is blank , skip it
|
||||
if pattern.match(line) or line.strip() == '':
|
||||
continue
|
||||
# add this subtitle line to subtitles list, remove the trailing spaces and line change
|
||||
subtitles.append(line.strip())
|
||||
|
||||
# remove duplicated subtitles
|
||||
subtitles = list(dict.fromkeys(subtitles))
|
||||
|
||||
# save the subtitles as a txt file
|
||||
with open(txt_filepath, 'w', encoding='utf-8') as f:
|
||||
for line in subtitles:
|
||||
if line:
|
||||
f.write(line + '\n')
|
||||
|
||||
except IOError:
|
||||
print(f"Could not read file: {subtitle_file}")
|
||||
|
||||
# scrape a webpage and perform OCR on images
|
||||
def scrape_and_ocr_forum(url, doc):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
text_elements = soup.find_all(['h1', 'h2', 'h3', 'p', 'li'])
|
||||
for element in text_elements:
|
||||
doc.add_paragraph(element.get_text())
|
||||
|
||||
image_elements = soup.find_all('img')
|
||||
for image in image_elements:
|
||||
if 'src' not in image.attrs:
|
||||
continue
|
||||
image_url = image['src']
|
||||
if image_url.startswith('http'):
|
||||
if not image_url.endswith('.svg') and not image_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in image_url:
|
||||
continue
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
|
||||
if ocr_text != ' ' and ocr_text != '':
|
||||
cleaned_string = ''.join(c for c in ocr_text if valid_xml_char_ordinal(c))
|
||||
doc.add_paragraph(cleaned_string)
|
||||
|
||||
def superuser_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# set up the markdown document
|
||||
markdown_content = ""
|
||||
|
||||
# get the question title and body
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
if question:
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# get all answers
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# deal with images and perform OCR
|
||||
all_img_tags = question.find_all('img') + [img for answer in answers for img in answer.find_all('img')]
|
||||
for img_tag in all_img_tags:
|
||||
image_src = img_tag.get('src') or img_tag.get('data-src') # Superuser uses lazy loading
|
||||
if image_src and image_src.startswith('http'):
|
||||
img_response = requests.get(image_src, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # if the OCR result is not empty, add it to the markdown content
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
def stack_overflow_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# set up the markdown document
|
||||
markdown_content = ""
|
||||
|
||||
# get the question title and body
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# get all answers
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# deal with images and perform OCR
|
||||
all_img_tags = soup.find_all('img')
|
||||
for img_tag in all_img_tags:
|
||||
image_url = img_tag['src']
|
||||
if image_url.startswith('http') and (image_url.endswith('.svg') or image_url.endswith('.png')): # 确保图片URL有效
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
def scrape_webpage_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
articles = soup.find_all('article') or soup.find_all('main') or soup.find_all('div', {'class': 'lia-message-body-content'})
|
||||
|
||||
if not articles:
|
||||
return
|
||||
|
||||
markdown_content = ''
|
||||
|
||||
# scrape the webpage and perform OCR on images
|
||||
for article in articles:
|
||||
for child in article.recursiveChildGenerator():
|
||||
# if this is an image, perform OCR
|
||||
if child.name == 'img':
|
||||
img_url = child.get('src')
|
||||
if not img_url.startswith(('http:', 'https:')):
|
||||
img_url = '{}{}'.format(url, img_url)
|
||||
if not img_url.endswith('.svg') and not img_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in img_url:
|
||||
continue
|
||||
try:
|
||||
img_response = requests.get(img_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += '\n```plaintext\n{}\n```\n'.format(ocr_text.strip())
|
||||
continue
|
||||
except PIL.UnidentifiedImageError:
|
||||
print("unidentified image")
|
||||
|
||||
# Not an image, so continue recursively calling function
|
||||
if child.name is None:
|
||||
continue
|
||||
|
||||
html_str = str(child)
|
||||
markdown_content += md(html_str) + '\n\n'
|
||||
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
# process a URL and save the file
|
||||
def process_url(url, doc_id, app):
|
||||
doc_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.md"
|
||||
txt_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.txt"
|
||||
doc = Document()
|
||||
|
||||
if 'youtube.com' in url or 'youtu.be' in url:
|
||||
download_and_clean_youtube_subtitles(url, txt_filepath)
|
||||
elif 'superuser.com' in url:
|
||||
superuser_to_markdown(url, doc_filepath)
|
||||
elif 'stackoverflow.com' in url:
|
||||
stack_overflow_to_markdown(url, doc_filepath)
|
||||
else:
|
||||
scrape_webpage_to_markdown(url, doc_filepath)
|
||||
|
||||
# read the CSV file and process each URL
|
||||
csv_filepath = './Get_Source_Doc - Sheet1.csv'
|
||||
with open(csv_filepath, 'r', newline='', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
cnt = 55
|
||||
for row in reader:
|
||||
if cnt>0:
|
||||
cnt -= 1
|
||||
continue
|
||||
process_url(row['Source'], row['id'], row['InvolvedApp'])
|
||||
print(row)
|
||||
@@ -1,293 +0,0 @@
|
||||
import csv
|
||||
import os
|
||||
import io
|
||||
import fitz
|
||||
import yt_dlp
|
||||
from docx import Document
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
from io import BytesIO
|
||||
from docx import Document
|
||||
import re
|
||||
import markdownify
|
||||
from markdownify import markdownify as md
|
||||
|
||||
def download_pdf(url):
|
||||
response = requests.get(url)
|
||||
response.raise_for_status() # 确保请求是成功的
|
||||
return io.BytesIO(response.content)
|
||||
|
||||
def pdf_to_markdown(pdf_stream, markdown_path):
|
||||
document = fitz.open(stream=pdf_stream, filetype="pdf")
|
||||
markdown_content = ""
|
||||
|
||||
for page_number in range(len(document)):
|
||||
page = document[page_number]
|
||||
text = page.get_text()
|
||||
markdown_content += text + "\n\n"
|
||||
|
||||
# 提取图片并添加到 Markdown 文件。图片被保存在同一目录下
|
||||
image_list = page.get_images(full=True)
|
||||
if image_list:
|
||||
markdown_content += f"### Page {page_number + 1} Images\n"
|
||||
|
||||
for img_index, image in enumerate(image_list, start=1):
|
||||
# 提取图片
|
||||
xref = image[0]
|
||||
base64_image = document.extract_image(xref)
|
||||
image_bytes = base64_image["image"]
|
||||
|
||||
# 写入图片到磁盘
|
||||
image_filename = f"output_image_page_{page_number + 1}_{img_index}.png"
|
||||
image_abs_path = os.path.join(os.path.dirname(markdown_path), image_filename)
|
||||
with open(image_abs_path, "wb") as image_file:
|
||||
image_file.write(image_bytes)
|
||||
|
||||
# 在 Markdown 文件中添加图片引用
|
||||
markdown_content += f"\n\n"
|
||||
|
||||
with open(markdown_path, "w", encoding="utf-8") as md_file:
|
||||
md_file.write(markdown_content)
|
||||
|
||||
document.close()
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
codepoint = ord(c)
|
||||
# conditions ordered by presumed frequency
|
||||
return (
|
||||
0x20 <= codepoint <= 0xD7FF or
|
||||
codepoint in (0x9, 0xA, 0xD) or
|
||||
0xE000 <= codepoint <= 0xFFFD or
|
||||
0x10000 <= codepoint <= 0x10FFFF
|
||||
)
|
||||
|
||||
def download_and_clean_youtube_subtitles(video_url, txt_filepath):
|
||||
# 设置yt-dlp库的选项来下载字幕
|
||||
subtitles_path = txt_filepath[0:-4]
|
||||
ydl_opts = {
|
||||
'skip_download': True,
|
||||
'writesubtitles': True,
|
||||
'writeautomaticsub': True, # 如果视频没有字幕,尝试下载自动生成的字幕
|
||||
'subtitleslangs': ['en'], # 下载英文字幕
|
||||
'outtmpl': f'{subtitles_path}.%(ext)s', # 确保保存到可写目录
|
||||
'quiet': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# 获取视频信息,下载字幕文件
|
||||
ydl.download([video_url])
|
||||
subtitle_file = f'{subtitles_path}.en.vtt'
|
||||
|
||||
# 读取下载的字幕文件
|
||||
subtitles = []
|
||||
try:
|
||||
with open(subtitle_file, 'r', encoding='utf-8') as file:
|
||||
# 读取所有行
|
||||
lines = file.readlines()
|
||||
|
||||
# 正则表达式匹配时间戳和其他不相关的标记
|
||||
pattern = re.compile(r'(\d+:\d\d:\d\d.\d+ --> \d+:\d\d:\d\d.\d+)|(\s*<[^>]+>)')
|
||||
|
||||
# 去除时间戳和HTML标签等,只保留字幕文本
|
||||
lines = [re.sub(pattern, '', line).strip() for line in lines if line.strip() and not pattern.match(line)]
|
||||
|
||||
# 清洗字幕
|
||||
for line in lines:
|
||||
# 如果这是一个时间线或者其他不需要的信息,跳过它
|
||||
if pattern.match(line) or line.strip() == '':
|
||||
continue
|
||||
# 添加到字幕列表,同时去除愈加和前导空白符
|
||||
subtitles.append(line.strip())
|
||||
|
||||
# 去除可能的重复行
|
||||
subtitles = list(dict.fromkeys(subtitles))
|
||||
|
||||
# 保存至txt文件
|
||||
with open(txt_filepath, 'w', encoding='utf-8') as f:
|
||||
for line in subtitles:
|
||||
if line: # 避免写入空行
|
||||
f.write(line + '\n')
|
||||
|
||||
except IOError:
|
||||
print(f"Could not read file: {subtitle_file}")
|
||||
|
||||
# 爬取论坛内容,对图片进行OCR处理,并保存为.docx文件
|
||||
def scrape_and_ocr_forum(url, doc):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
text_elements = soup.find_all(['h1', 'h2', 'h3', 'p', 'li'])
|
||||
for element in text_elements:
|
||||
doc.add_paragraph(element.get_text())
|
||||
|
||||
image_elements = soup.find_all('img')
|
||||
for image in image_elements:
|
||||
if 'src' not in image.attrs:
|
||||
continue
|
||||
image_url = image['src']
|
||||
if image_url.startswith('http'):
|
||||
if not image_url.endswith('.svg') and not image_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in image_url:
|
||||
continue
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
|
||||
if ocr_text != ' ' and ocr_text != '':
|
||||
cleaned_string = ''.join(c for c in ocr_text if valid_xml_char_ordinal(c))
|
||||
doc.add_paragraph(cleaned_string)
|
||||
|
||||
def superuser_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 创建Markdown文件的内容
|
||||
markdown_content = ""
|
||||
|
||||
# 获取问题标题和内容
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 获取所有回答
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 处理图片并执行OCR
|
||||
all_img_tags = question.find_all('img') + [img for answer in answers for img in answer.find_all('img')]
|
||||
for img_tag in all_img_tags:
|
||||
image_src = img_tag.get('src') or img_tag.get('data-src') # Superuser使用延迟加载的图片
|
||||
if image_src and image_src.startswith('http'):
|
||||
img_response = requests.get(image_src, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # 如果OCR结果非空,则添加到Markdown内容中
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
# 将Markdown内容写入文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
def stack_overflow_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 创建Markdown文件的内容
|
||||
markdown_content = ""
|
||||
|
||||
# 获取问题标题和内容
|
||||
question = soup.find('div', {'id': 'question'})
|
||||
|
||||
question_title = soup.find('h1').get_text(strip=True)
|
||||
if question:
|
||||
|
||||
question_body = question.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += f"# {question_title}\n\n" + markdownify.markdownify(question_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 获取所有回答
|
||||
answers = soup.find_all('div', {'class': 'answer'})
|
||||
for answer in answers:
|
||||
answer_body = answer.find('div', {'class': 's-prose js-post-body'}).prettify()
|
||||
markdown_content += markdownify.markdownify(answer_body, heading_style="ATX") + "\n\n"
|
||||
|
||||
# 处理图片并执行OCR
|
||||
all_img_tags = soup.find_all('img')
|
||||
for img_tag in all_img_tags:
|
||||
image_url = img_tag['src']
|
||||
if image_url.startswith('http') and (image_url.endswith('.svg') or image_url.endswith('.png')): # 确保图片URL有效
|
||||
img_response = requests.get(image_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip(): # 如果OCR结果非空,则添加到Markdown内容中
|
||||
markdown_content += "```\n" + ocr_text.strip() + "\n```\n\n"
|
||||
|
||||
# 将Markdown内容写入文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
def scrape_webpage_to_markdown(url, doc_filepath):
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# 假设文章内容在 HTML 的 'article' 标签中,根据实际页面结构调整
|
||||
articles = soup.find_all('article') or soup.find_all('main') or soup.find_all('div', {'id':'steps'}, {'class':'section_text'}) # 或其他包含主要内容的HTML标签
|
||||
|
||||
if not articles:
|
||||
articles = soup.find_all('div', {'class': 'lia-message-body-content'})
|
||||
|
||||
markdown_content = ''
|
||||
|
||||
# 抓取所有图文信息
|
||||
for article in articles:
|
||||
for child in article.recursiveChildGenerator():
|
||||
# 如果是图片,则进行OCR
|
||||
if child.name == 'img':
|
||||
img_url = child.get('src')
|
||||
if not img_url:
|
||||
continue
|
||||
if not img_url.startswith(('http:', 'https:')):
|
||||
img_url = '{}{}'.format(url, img_url)
|
||||
if not img_url.endswith('.svg') and not img_url.endswith('.png'):
|
||||
continue
|
||||
if 'neveragain.allstatics.com/2019/assets/icon/logo' in img_url:
|
||||
continue
|
||||
print(img_url)
|
||||
try:
|
||||
img_response = requests.get(img_url, stream=True)
|
||||
img = Image.open(BytesIO(img_response.content))
|
||||
ocr_text = pytesseract.image_to_string(img)
|
||||
if ocr_text.strip():
|
||||
markdown_content += '\n```plaintext\n{}\n```\n'.format(ocr_text.strip())
|
||||
continue
|
||||
except PIL.UnidentifiedImageError:
|
||||
print("unidentified image")
|
||||
# 不是标签,可能是NavigableString或其他
|
||||
if child.name is None:
|
||||
continue
|
||||
# 抓取标签并转换为Markdown
|
||||
html_str = str(child)
|
||||
markdown_content += md(html_str) + '\n\n'
|
||||
|
||||
# 写入markdown文件
|
||||
with open(doc_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
# 处理单个URL
|
||||
def process_url(url, doc_id, app):
|
||||
doc_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.md"
|
||||
txt_filepath = f"/content/drive/MyDrive/SourceDoc/{doc_id}_{app}.txt"
|
||||
doc = Document()
|
||||
|
||||
if 'youtube.com' in url or 'youtu.be' in url:
|
||||
download_and_clean_youtube_subtitles(url, txt_filepath)
|
||||
elif url.endswith('.pdf'):
|
||||
pdf_stream = download_pdf(url)
|
||||
pdf_to_markdown(pdf_stream, doc_filepath)
|
||||
elif 'superuser.com' in url or 'askubuntu.com' in url:
|
||||
superuser_to_markdown(url, doc_filepath)
|
||||
elif 'stackoverflow.com' in url:
|
||||
stack_overflow_to_markdown(url, doc_filepath)
|
||||
else:
|
||||
scrape_webpage_to_markdown(url, doc_filepath)
|
||||
|
||||
# 读取CSV文件中的数据并执行对应操作
|
||||
csv_filepath = '/content/Get_Source_Doc - Sheet1.csv' # 更新为你的CSV文件实际路径
|
||||
with open(csv_filepath, 'r', newline='', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
cnt = 176
|
||||
for row in reader:
|
||||
if cnt>0:
|
||||
cnt -= 1
|
||||
continue
|
||||
process_url(row['Source'], row['id'], row['InvolvedApp'])
|
||||
print(row)
|
||||
@@ -1,17 +0,0 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "https://tiktok-download-video1.p.rapidapi.com/feedSearch"
|
||||
|
||||
querystring = {"keywords":"Ubuntu desktop","count":"10","cursor":"0","region":"US","publish_time":"0","sort_type":"1"}
|
||||
|
||||
headers = {
|
||||
"X-RapidAPI-Key": "YOUR_API_KEY",
|
||||
"X-RapidAPI-Host": "tiktok-download-video1.p.rapidapi.com"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
with open("./record_1.json", "w") as f:
|
||||
json.dump(response.json(), f)
|
||||
@@ -1,506 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"processed_time": 1.2181,
|
||||
"data": {
|
||||
"videos": [
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cfdgkhbc77ufdvjieor0",
|
||||
"video_id": "7195358717750070555",
|
||||
"region": "ID",
|
||||
"title": "#CapCut comparing the pros and cons of two operating system: #windows and #linux. \ud83d\udc68\u200d\ud83d\udcbb Unleash the power of #Linux ! Experience a better, more efficient computing with this open-source OS #windows11 #windows10 #ubuntu #programming #computerscience #opensource #technology #FaktaProgrammer #jagocoding #debian #malware #hacking #ProgrammerImut ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/2410a4ce346f409ca1f0ba4b543ce7c3_1675300016~tplv-dmt-logom:tos-useast2a-pv-0037-aiso/77578d1e8c9a4efaaa057636e35ab262.image?x-expires=1703127600&x-signature=9GzGgxD2brvooV5TsmsuyCFqloU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/5904cc8b48a14d4081f05e97dd006328_1675300016~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=zKuLeet7txU5bAPZCeWTFRYHZas%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v16m.tiktokcdn-us.com/385a61f83cddd3276b6b3955ae059c98/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oIYbF3pDWeVcQUPIUB8jADjnQ2PNBDBLbw2Cne/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1398&bt=699&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NDQzaDw1N2Q8OTpoNzNkPEBpM3JseWk6ZmtqaTMzZjgzM0BjM2EtXzRgNjIxXjJeNWJhYSNvYmdhcjRfaGFgLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/f6c45472d69d11105cd4f55ebb8e7d13/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037c001-aiso/oI2pBQPnjBbeYpn8DBFwAAI3CDcbb27QeVUKnF/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1124&bt=562&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=11&rc=ZzVlODZnZWdmM2k2OzhmZ0BpM3JseWk6ZmtqaTMzZjgzM0AtX2BfLjJgX2AxYGJjNDQzYSNvYmdhcjRfaGFgLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7195358744048438042.mp3",
|
||||
"music_info": {
|
||||
"id": "7195358701144902426",
|
||||
"title": "original sound - ferryops_",
|
||||
"play": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7195358744048438042.mp3",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/5b59cd09101f0a570efe722f5f1f64b1~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=w1qF6e5dmhhCgWPwiDvSJxZVQO4%3D",
|
||||
"author": "ferry",
|
||||
"original": true,
|
||||
"duration": 15,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 814198,
|
||||
"digg_count": 55739,
|
||||
"comment_count": 1876,
|
||||
"share_count": 1911,
|
||||
"download_count": 62,
|
||||
"create_time": 1675300014,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6924124906000303105",
|
||||
"unique_id": "ferryops_",
|
||||
"nickname": "ferry",
|
||||
"avatar": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/5b59cd09101f0a570efe722f5f1f64b1~c5_300x300.jpeg?x-expires=1703127600&x-signature=NNsWWsjrvJ206kY8qyFxiZE5k20%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000chd8vdbc77uea2obsidg",
|
||||
"video_id": "7231251211062758702",
|
||||
"region": "US",
|
||||
"title": "What would you do with this data? #computervision #machinelearning #objectdetection #csproject #softwareengineer ",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/ee1c7307d8ba46008d9a2f1002ffe99b_1683656884~tplv-dmt-logom:tos-useast5-i-0068-tx/5f1c1ee9247449aa8d86b36beeacc10d.image?x-expires=1703127600&x-signature=Wzp2xq0jwoPBoDrSEDydKP8RitY%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p19-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/c5b40f31562a4b1c9bc6e9066b8da76c_1683656884~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=q301jDESVmXKzurDSZEsAaY71u4%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 126,
|
||||
"play": "https://v16m.tiktokcdn-us.com/dd179aa8851feda2a2f84f6859d626ce/6582ae74/video/tos/useast5/tos-useast5-pve-0068-tx/osaeSLPIjUoAMkRbctIQWFnIjDbeDH1RCACekt/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1490&bt=745&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Ojg4OGZnPDtlZzY0Ojk6NEBpamxlNWg6Zmc7azMzZzczNEAxYV5iMy1gXjIxY2ExNi0yYSNhcGxecjRfc2FgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00010000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/b36feb41d71b86ed6e4e6ea7a72e71af/6582ae74/video/tos/useast5/tos-useast5-pve-0068-tx/oAIHCeIo1WkngCFIA1kYRbDfeIAjagtcq28oYQ/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1548&bt=774&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Mzw7Zjs2NjY1MzxpNTc3aEBpamxlNWg6Zmc7azMzZzczNEBfYy40XzBeNWAxMWEuNF5gYSNhcGxecjRfc2FgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00010000",
|
||||
"music": "https://sf19.tiktokcdn-us.com/obj/ies-music-tx/7231251227135281962.mp3",
|
||||
"music_info": {
|
||||
"id": "7231251193572477742",
|
||||
"title": "original sound - codingai",
|
||||
"play": "https://sf19.tiktokcdn-us.com/obj/ies-music-tx/7231251227135281962.mp3",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/69600b7c44fc05a3b118277409c5c6fb~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=eg%2B7HmT9JXLgeMm0WgzQHu9nh2I%3D",
|
||||
"author": "Eric",
|
||||
"original": true,
|
||||
"duration": 126,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 277284,
|
||||
"digg_count": 20971,
|
||||
"comment_count": 188,
|
||||
"share_count": 868,
|
||||
"download_count": 221,
|
||||
"create_time": 1683656883,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7044388915265668143",
|
||||
"unique_id": "codingai",
|
||||
"nickname": "Eric",
|
||||
"avatar": "https://p16-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/69600b7c44fc05a3b118277409c5c6fb~c5_300x300.jpeg?x-expires=1703127600&x-signature=2n0cRjUEqT4lHZcZkoKH0wSURVY%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000c9g4kbrc77u91sdh9jp0",
|
||||
"video_id": "7088747410289102086",
|
||||
"region": "IT",
|
||||
"title": "Come installare #ubuntu e far risorgere un vecchio pc #aletech",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e25e3d586f4c4cdfa18235f07b686497_1650477660~tplv-dmt-logom:tos-useast2a-v-0068/85ca53bad5e4402ba107aa9f88f3d396.image?x-expires=1703127600&x-signature=D18vkekhPxb8FvOOLcFLUcx9xLA%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/41390a6f86074b92b6f2c0e0f7862940_1650477670~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=jTNGQhpF1J2xAkQt9Y99KyBUK%2Fs%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 42,
|
||||
"play": "https://v16m.tiktokcdn-us.com/2911fa4f722b77bde13d550ed592bd72/6582ae20/video/tos/useast2a/tos-useast2a-ve-0068c003/386472d4c6fa4b479b2cf07c829bf268/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3076&bt=1538&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NDVlPDc0ZDNkZjo7ZGRnaUBpM21rdjw6ZmU3PDMzNzczM0AwMF5jNi0yNV4xNDJiXzAyYSNtNmEucjRvaGRgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/9cc6a5d30e0c8948fa294f0027bd0c53/6582ae20/video/tos/useast2a/tos-useast2a-ve-0068c003/9262ec5e631d49e399d3a4bec28db2a7/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3068&bt=1534&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NTxmOmVoO2g0aDk3ZzlkZkBpM21rdjw6ZmU3PDMzNzczM0A0NDU1XzUuNi4xX15gYDQuYSNtNmEucjRvaGRgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/38acb76cefbb49a187e366d26cf62dcc",
|
||||
"music_info": {
|
||||
"id": "6756231813640751105",
|
||||
"title": "Better Days",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/38acb76cefbb49a187e366d26cf62dcc",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/f2bad9596ac24f3a891c8fd1d5f1ac14.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Better Days"
|
||||
},
|
||||
"play_count": 96398,
|
||||
"digg_count": 6714,
|
||||
"comment_count": 221,
|
||||
"share_count": 378,
|
||||
"download_count": 771,
|
||||
"create_time": 1650477624,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6903482507896587266",
|
||||
"unique_id": "ale_tech_",
|
||||
"nickname": "Ale_tech",
|
||||
"avatar": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/81f0a65e38808852ad043323d2ce9f1f~c5_300x300.jpeg?x-expires=1703127600&x-signature=djzLzF0pH%2BAV9a8qJvTZh8WNYJE%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v07025g50000c1ld595r2810d6r02sr0",
|
||||
"video_id": "6947597057498680578",
|
||||
"region": "ID",
|
||||
"title": "Reply to @efryday1 segini aja ya, klo full sampe instalasi ga cukup \ud83d\ude01 #linux #ubuntu #fyp",
|
||||
"cover": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-p-0037/64ad02f4cfed4bd6961c1c1c992f7df1_1617613505~tplv-dmt-logom:tos-alisg-i-0000/d375b8ca573c48e18f7f19175091a0a2.image?x-expires=1703127600&x-signature=GcUS%2Bet8IeNtkQKFSCTILJMULwU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-p-0037/f3fb7b0dc7aa4337bcfd1badd15b403e_1617613505~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=Zea7B3dtoIkndr3j5gE0ehRiM6g%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 31,
|
||||
"play": "https://v16m.tiktokcdn-us.com/a46680841f4e969f7c489dd568c85b63/6582ae15/video/tos/alisg/tos-alisg-pve-0037/efc3dc88570e4b408f460da5b30fb8cf/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2154&bt=1077&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=N2lpaGg2ODpmZDM7aGZkOkBpM3YzOTM7dTxnNDMzODgzM0A0Yl9fNDQvXl8xLTEzMS4vYSNvL29hLi8yMmlgLS1kLzRzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/6ec40d75d2efc39bbbc62d727e37dcb5/6582ae15/video/tos/alisg/tos-alisg-pve-0037/a773953f390642158983d23034e27b84/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2146&bt=1073&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=MzNnOGk2PDMzaDQ2Nzg8ZUBpM3YzOTM7dTxnNDMzODgzM0AuMTIxMF42Xl4xYl8tYV9fYSNvL29hLi8yMmlgLS1kLzRzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o4DXDrZtQBCbzoc9djRgk49nQBelNUnnqBesLO",
|
||||
"music_info": {
|
||||
"id": "6936177291449862145",
|
||||
"title": "Terpesona",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o4DXDrZtQBCbzoc9djRgk49nQBelNUnnqBesLO",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/3c59e86071a6488386f0eb8d0520c80e.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Terpesona"
|
||||
},
|
||||
"play_count": 90199,
|
||||
"digg_count": 5316,
|
||||
"comment_count": 138,
|
||||
"share_count": 56,
|
||||
"download_count": 329,
|
||||
"create_time": 1617613504,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6568018206484742146",
|
||||
"unique_id": "nvmbrxi",
|
||||
"nickname": "adit, iya pasaran iya",
|
||||
"avatar": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/468629c8128b024d5787e670f9b42cf5~c5_300x300.jpeg?x-expires=1703127600&x-signature=t7lOKDBIC5r4n%2BalT1G8j4jKHGQ%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000ciktif3c77ub4lij07ag",
|
||||
"video_id": "7253570500482321669",
|
||||
"region": "DO",
|
||||
"title": "If you love coding leave your \u2764\ufe0f #softwareengineer #softwaredeveloper #focus #fullstackdeveloper #ubuntudesktop #ubuntu #cybersecurity #reactnativedeveloper #androiddev #androiddeveloper #frontenddeveloper credits: mobiledevpro \ud83d\ude4f",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/5290f82af0a041f4ae3482161633c5ec_1688853494?x-expires=1703127600&x-signature=Qp1zu10vuLK%2BNq%2FcoElwf7hDO%2Bo%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/4c4a0d1a522c47e9bdd2b4e033cba110_1688853493~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=zy%2BADnksas488G1HYiROxwdXsKo%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 11,
|
||||
"play": "https://v16m.tiktokcdn-us.com/84404bbc3b0d32ee0baea347f3a1d0ef/6582ae01/video/tos/useast2a/tos-useast2a-ve-0068c001/ocfAhU8g7ZJ81dITBenAQkqsRlkQtB9EPdDTbX/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1368&bt=684&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NjlpOzxlMzhoOjgzOThpZEBpajptb2U6Zml3bDMzNzczM0BiYC9jYjMuNjIxM2IwMy4yYSNeLWYxcjQwZmhgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/a2d5770f1c2b3957e6394ba7426f6d67/6582ae01/video/tos/maliva/tos-maliva-ve-0068c801-us/o4JUKZ6bhM7pCAKIC4rnZCAeej5fUS2y1gmPJH/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1418&bt=709&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NTw4Ojk2NWlkODtkaGU4ZEBpajptb2U6Zml3bDMzNzczM0BjYjZgXi5hXzIxMC9fL2AuYSNeLWYxcjQwZmhgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7232735770502859546.mp3",
|
||||
"music_info": {
|
||||
"id": "7232735763421596442",
|
||||
"title": "All my Life Lil Durk DrillyEditz",
|
||||
"play": "https://sf16-ies-music.tiktokcdn.com/obj/ies-music-aiso/7232735770502859546.mp3",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-euttp/b05fa4d33296c39c75fbc760a3f3ec5c~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=8MeRalgtFOdb9QfxLhB2QcnIToo%3D",
|
||||
"author": "DrillyEditz\ud83e\udd77\ud83c\udffd",
|
||||
"original": true,
|
||||
"duration": 16,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 35581,
|
||||
"digg_count": 2212,
|
||||
"comment_count": 76,
|
||||
"share_count": 26,
|
||||
"download_count": 3,
|
||||
"create_time": 1688853492,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7150316965491721221",
|
||||
"unique_id": "systemf4iled",
|
||||
"nickname": "System F4iled",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/742fa96d7edbfd240757b83abf1dee0b~c5_300x300.jpeg?x-expires=1703127600&x-signature=q8rwCWfCZfWurxF6cfYOaSFciNo%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbsv2tjc77uftkprfpdg",
|
||||
"video_id": "7131996966136958213",
|
||||
"region": "TH",
|
||||
"title": "Linux > Windows as always #Windows #Windows10 #Linux #Ubuntu #UbuntuMATE #computer #fyp",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/421b9598296b4bc6923c82eeb0f74104_1660547458~tplv-dmt-logom:tos-useast2a-v-0068/ab4d5c65921f4556be5bd4218a05e684.image?x-expires=1703127600&x-signature=mKiZ6hytGR6zx7ek4ORgtryE6w8%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e8f1878fc8ba466aa5af013169e33e84_1660547459~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=6L2yZY%2FL03UOop%2FV7q7SX1Q64hI%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v19.tiktokcdn-us.com/edc39bdef7b4266e334ae233f724aeb6/6582ae05/video/tos/useast2a/tos-useast2a-pve-0068/4d8f2b0e1f2b4f60ad84a60fa124bfc3/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3178&bt=1589&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=NWk6O2hkaDdkM2g7ZTg6PEBpanN1bmk6Znd5ZTMzNzczM0AyYl9eLTReXzYxXzYvLTI0YSNhY21xcjRnL3BgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/c882763d2ef55a77003b1dace8fd7fd1/6582ae05/video/tos/maliva/tos-maliva-ve-0068c801-us/512ba1d0e8824c9087d6aea78ffa3a6b/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3388&bt=1694&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Ojc5NGk7aTdnM2RkOjQ2ZkBpanN1bmk6Znd5ZTMzNzczM0AtXmJhYC5fXy0xXzItYzRhYSNhY21xcjRnL3BgLS1kMTZzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/ocvaqFeexInKSUDFFPAAbfDlG4UPzTODg5VBEC",
|
||||
"music_info": {
|
||||
"id": "6915348665175526145",
|
||||
"title": "Can You Feel My Heart",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/ocvaqFeexInKSUDFFPAAbfDlG4UPzTODg5VBEC",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/b7c28602318840fbbb4f4b0f2ed7ddf9.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 15,
|
||||
"album": "Can You Feel My Heart"
|
||||
},
|
||||
"play_count": 36974,
|
||||
"digg_count": 1078,
|
||||
"comment_count": 202,
|
||||
"share_count": 23,
|
||||
"download_count": 3,
|
||||
"create_time": 1660547455,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6988352212003243009",
|
||||
"unique_id": "hannor_smith",
|
||||
"nickname": "han.flac \ud83c\udf1f\ud83c\udfa7\ud83c\udfcd\ufe0f",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/58167255ee1d05d70dffc59ec0286e96~c5_300x300.jpeg?x-expires=1703127600&x-signature=N3KkHMBaqDBqVcHCt833OonyJ58%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000cb4rr83c77u2coev35og",
|
||||
"video_id": "7118429570973519150",
|
||||
"region": "US",
|
||||
"title": "If you haven't tried Linux yet, Ubuntu should be your first \ud83e\udd79\ud83d\udcbb #linuxdistro #ubuntulinux #oldlaptopmotherboard #oldpchardware #linux",
|
||||
"cover": "https://p19-sign.tiktokcdn-us.com/obj/tos-useast5-p-0068-tx/57a059b3776f4deeaa4a7297dd9d46fa_1657388545?x-expires=1703127600&x-signature=2HaAe082pCt55gBKb9YvXCyITyw%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/8680ac0cc03840df9ff6f003cbeb74f6_1657388545~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=s3Xz2qsI2um65ZUbw3Lg8SQYo20%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 30,
|
||||
"play": "https://v19.tiktokcdn-us.com/607a59a261b518e59c26873e2d17f22d/6582ae13/video/tos/useast5/tos-useast5-pve-0068-tx/948fbacccb394ae2b722a1e39f3daa59/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1524&bt=762&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aDY4PDw5OjM1ZWlkNDxpaUBpajh5cjU6Zjt1ZTMzZzczNEAzXi4tYl4uNi0xLjBeNC5hYSNsMGJgcjQwbzFgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/eee0d5c470e85e78957dc78605caf792/6582ae13/video/tos/useast5/tos-useast5-ve-0068c001-tx/27add3d74a14413091513d4c7a3bd553/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1522&bt=761&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aDppOTdmPDY2OzszaDtnOkBpajh5cjU6Zjt1ZTMzZzczNEBgL2IzNDFhNWAxNWFgYTU1YSNsMGJgcjQwbzFgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oc1hI8e1jwQ9aPyoBOnCUc6YZwjgZBbt1GDfbY",
|
||||
"music_info": {
|
||||
"id": "6778968637492430849",
|
||||
"title": "Blue Blood",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oc1hI8e1jwQ9aPyoBOnCUc6YZwjgZBbt1GDfbY",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/c24e133f8d134031b03a673aa747ed6d.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "It's Always Sunny In Philadelphia (Music from the Original FX Series)"
|
||||
},
|
||||
"play_count": 30464,
|
||||
"digg_count": 817,
|
||||
"comment_count": 29,
|
||||
"share_count": 34,
|
||||
"download_count": 84,
|
||||
"create_time": 1657388542,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7113859000824316974",
|
||||
"unique_id": "buggintech",
|
||||
"nickname": "buggintech",
|
||||
"avatar": "https://p16-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/e390a937f6bc4281817b391361497762~c5_300x300.jpeg?x-expires=1703127600&x-signature=nyeyvxtxVn%2F0SE%2Fn7Wnavk3wGjE%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000cgi7l53c77u11maehifg",
|
||||
"video_id": "7216028231185616171",
|
||||
"region": "US",
|
||||
"title": "Replying to @fltlnsb I say all this with extreme confidence, as I\u2019ve spent the last 12 years supporting users ages 30-70. People that grew up with computers around, some who have used computers for 20 years. Telling someone \u2018you need to learn an entire new platform and convert your PDF\u2019s with command line because it\u2019s better\u2019 is ridiculous. Can YOU do it faster with CLI? Yes. Does that mean everybody should? Absolutely not. Here in 2023, the vast majority of people in the work force and ones who own business are barely getting by with the Windows computers they have. Microsoft has a chokehold on the end user facing side of business/corporate/enterprise business. Uprooting their infrastructure to implement a product that will change everything they do from a day to day because \u2018it\u2019s better\u2019 - sorry, not a business expense anyone is willing to swallow. You can spend 15 hours a week for a few weeks learning the OS and figuring out whats good about it. Other people do not. Time is money. and is business, just because you csn - doesnt mean you should. #ubuntu #linux #computers #naaackers ",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/0485fb4c1f2549c8987ba323ed26993f_1680112511~tplv-dmt-logom:tos-useast5-i-0068-tx/798594786eff41458cbbc4f6da8a1476.image?x-expires=1703127600&x-signature=ZARZhRKsQwA%2FgqGM4p6Y%2BD%2F%2FRpI%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/aff34ca418e1453390f96584faae5535_1680112510~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=qiKmXCjMnnE%2B8P68jaEydOsTtEU%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 58,
|
||||
"play": "https://v19.tiktokcdn-us.com/ac39a5443137abb7743118c8e6c38046/6582ae30/video/tos/useast5/tos-useast5-ve-0068c002-tx/126f8a804a18420cb9f9a0362aa3ac6c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1760&bt=880&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=Mzk6NGc7Zzg3NDs2O2Q4OkBpamxocDQ6Zjg6ajMzZzczNEAxNC8wMC1hNmAxLjYwXzI2YSNjZV4ucjQwaWZgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/d9bb3ef4e9ef24d612514d12c451b6cc/6582ae30/video/tos/useast5/tos-useast5-ve-0068c004-tx/f40dd86127a2452fa9b021f295880c2c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1862&bt=931&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=O2g0aTM1Zjw1NWk7OTZkaEBpamxocDQ6Zjg6ajMzZzczNEBeNTJjNWJfXl4xNDZeYTBfYSNjZV4ucjQwaWZgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7216028260373662507.mp3",
|
||||
"music_info": {
|
||||
"id": "7216028213967997739",
|
||||
"title": "original sound - naaackers",
|
||||
"play": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7216028260373662507.mp3",
|
||||
"cover": "https://p19-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/99794bd713d80b1c2b65aff94b0e6500~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=a6w1ZerLtyqMcKa2H0ioqXBy%2FxQ%3D",
|
||||
"author": "Naaackers",
|
||||
"original": true,
|
||||
"duration": 58,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 7621,
|
||||
"digg_count": 528,
|
||||
"comment_count": 227,
|
||||
"share_count": 5,
|
||||
"download_count": 2,
|
||||
"create_time": 1680112522,
|
||||
"anchors": [
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action_type": 1,
|
||||
"icon": {
|
||||
"height": 720,
|
||||
"uri": "tiktok-obj/28px_primary_create_onDark3x.png",
|
||||
"url_list": [
|
||||
"https://p16-sg.tiktokcdn.com/obj/tiktok-obj/28px_primary_create_onDark3x.png?biz_tag=anchor.effect"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 720
|
||||
},
|
||||
"schema": "shoot"
|
||||
}
|
||||
],
|
||||
"component_key": "anchor_effect",
|
||||
"description": "Effects",
|
||||
"extra": "{\"effect_source\":0,\"is_commerce\":0}",
|
||||
"icon": {
|
||||
"height": 720,
|
||||
"uri": "tiktok-obj/20px_anchor_effect3x.png",
|
||||
"url_list": [
|
||||
"https://p16-sg.tiktokcdn.com/obj/tiktok-obj/20px_anchor_effect3x.png?biz_tag=anchor.effect"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 720
|
||||
},
|
||||
"id": "454747",
|
||||
"keyword": "Green Screen Video",
|
||||
"log_extra": "{\"anchor_id\":\"454747\",\"anchor_name\":\"Green Screen Video\",\"anchor_type\":\"prop\"}",
|
||||
"thumbnail": {
|
||||
"height": 64,
|
||||
"uri": "d846c3603d36559c76691965ba808340",
|
||||
"url_list": [
|
||||
"https://lf16-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340",
|
||||
"https://lf21-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340",
|
||||
"https://lf19-effectcdn-va.tiktokcdn.com/obj/ies-fe-effect-va/d846c3603d36559c76691965ba808340"
|
||||
],
|
||||
"url_prefix": null,
|
||||
"width": 64
|
||||
},
|
||||
"type": 28
|
||||
}
|
||||
],
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6757790857447539718",
|
||||
"unique_id": "naaackers",
|
||||
"nickname": "Naaackers",
|
||||
"avatar": "https://p19-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/99794bd713d80b1c2b65aff94b0e6500~c5_300x300.jpeg?x-expires=1703127600&x-signature=f7mkopk%2F2EG%2FpA98qcgATY8nj80%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cfaidr3c77u090l3co8g",
|
||||
"video_id": "7193698776148725019",
|
||||
"region": "TH",
|
||||
"title": "Life of a Linux user \ud83d\udc27 #linux #ubuntu #computer #laptop #tech #techtok ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/obj/tos-useast2a-p-0037-aiso/6fb0705918f0413b9127aa0c2c95c0ed_1674913526?x-expires=1703127600&x-signature=yNngPeJxRl5AB6rIiA3EjO%2BO6Gg%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/908c47399ad2445b8720a5c0c49e45fa_1674913529~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=yt9L2hrWPZ3Z1mbX9tVT8KDOeB0%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 15,
|
||||
"play": "https://v19.tiktokcdn-us.com/3e418870739bcd9b1a1244bfa89d4b8f/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oMRKA6h7oCSVjVnvBCN2JToIzNA9fhQY2AIxpU/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2328&bt=1164&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aGYzNDs5ZDlnNzhmOjk8NEBpanI2MzM6ZnVsaTMzZjgzM0AvMDM0LzQxXjExMGFhNTIyYSM1YGk2cjQwYV5gLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/1e5ce0481d836bbef971d1c12ebefd60/6582ae05/video/tos/useast2a/tos-useast2a-pve-0037-aiso/o43NInhzZSNh2AoQixpVBYRUbCf2CKA6o2A6Wo/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2616&bt=1308&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=ZTQ1OjRlOTc5NDg5OzRmPEBpanI2MzM6ZnVsaTMzZjgzM0A2MDE2MGJhNmAxMy8uLzYtYSM1YGk2cjQwYV5gLS1kL2Nzcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oYnmdQz4MBF51IDQzbZptpTBBaALqgveOLC1ek",
|
||||
"music_info": {
|
||||
"id": "6969850109240559617",
|
||||
"title": "Meow",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/oYnmdQz4MBF51IDQzbZptpTBBaALqgveOLC1ek",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/df24a43905f9413bb3596cb8c765bc16.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Meow"
|
||||
},
|
||||
"play_count": 29000,
|
||||
"digg_count": 458,
|
||||
"comment_count": 31,
|
||||
"share_count": 9,
|
||||
"download_count": 1,
|
||||
"create_time": 1674913523,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6988352212003243009",
|
||||
"unique_id": "hannor_smith",
|
||||
"nickname": "han.flac \ud83c\udf1f\ud83c\udfa7\ud83c\udfcd\ufe0f",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/58167255ee1d05d70dffc59ec0286e96~c5_300x300.jpeg?x-expires=1703127600&x-signature=N3KkHMBaqDBqVcHCt833OonyJ58%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v12044gd0000c9hnderc77u0gkpj1f80",
|
||||
"video_id": "7089641605862313262",
|
||||
"region": "US",
|
||||
"title": "#linux #sysadmin #systemadministrator #python #fyp\u30b7 #ubuntu #tech #computerscience #networkengineer #linux #bash",
|
||||
"cover": "https://p16-sign.tiktokcdn-us.com/obj/tos-useast5-p-0068-tx/f4004afdbfb1457fa72e54aeebbae658_1650685820?x-expires=1703127600&x-signature=baO3nkbdG73H%2FLlG%2BB%2B5E7Nff3k%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=2023122003034909B448A5A66536000675",
|
||||
"origin_cover": "https://p16-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/8995429c7adb447083dfa93330af88bd_1650685820~tplv-tiktokx-360p.webp?x-expires=1703127600&x-signature=CPnQVAqZ%2F2C8tS%2FmgBczNQzDnmM%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=2023122003034909B448A5A66536000675",
|
||||
"duration": 47,
|
||||
"play": "https://v19.tiktokcdn-us.com/0c2c6034ef9b8ebc826a41cd168a3bff/6582ae25/video/tos/useast5/tos-useast5-ve-0068c003-tx/e25fcce499b04eaf97928d4c70f27070/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1502&bt=751&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=M2loZDhkZTMzaDg1OzloOUBpM2ltbjM6ZmhxPDMzZzczNEAvYzEvMDEvXjExLy9gYDQ1YSM1Lm1kcjRvYWVgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/540ebc82370825c5bb5cac7605ba9166/6582ae25/video/tos/useast5/tos-useast5-pve-0068-tx/a06e405ea9f042eebc7d7d3432a5a3d3/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1498&bt=749&bti=NTY6QGo0QHM6OjZANDQuYCMucCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZNx0PD1J-wtxg9w8HGM5kEeC~&mime_type=video_mp4&qs=0&rc=aTo4N2QzaDxpaDs2OjRlOkBpM2ltbjM6ZmhxPDMzZzczNEA2MC0vMmAtXmAxMGI2XjZjYSM1Lm1kcjRvYWVgLS1kMS9zcw%3D%3D&l=2023122003034909B448A5A66536000675&btag=e00008000",
|
||||
"music": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7089641612577409835.mp3",
|
||||
"music_info": {
|
||||
"id": "7089641579396287278",
|
||||
"title": "original sound - stevenservo",
|
||||
"play": "https://sf16.tiktokcdn-us.com/obj/ies-music-tx/7089641612577409835.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/62713abc4ad5f8eb52869addf6611a92~c5_1080x1080.jpeg?x-expires=1703127600&x-signature=x8ba50BVMtG%2BVOppSeHHwPsi2x8%3D",
|
||||
"author": "StevenServo",
|
||||
"original": true,
|
||||
"duration": 47,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 4083,
|
||||
"digg_count": 427,
|
||||
"comment_count": 28,
|
||||
"share_count": 18,
|
||||
"download_count": 44,
|
||||
"create_time": 1650685819,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6941572883425166342",
|
||||
"unique_id": "stevenservo",
|
||||
"nickname": "StevenServo",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/62713abc4ad5f8eb52869addf6611a92~c5_300x300.jpeg?x-expires=1703127600&x-signature=sQFsgOJXOOxGQtHf4cwmfNrSKX4%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
}
|
||||
],
|
||||
"cursor": 10,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "https://tiktok-download-video1.p.rapidapi.com/feedSearch"
|
||||
|
||||
querystring = {"keywords":"VS code","count":"10","cursor":"0","region":"US","publish_time":"0","sort_type":"1"}
|
||||
|
||||
headers = {
|
||||
"X-RapidAPI-Key": "YOUR_API_KEY",
|
||||
"X-RapidAPI-Host": "tiktok-download-video1.p.rapidapi.com"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
with open("./record.json", "w") as f:
|
||||
json.dump(response.json(), f)
|
||||
@@ -1,373 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"processed_time": 1.2462,
|
||||
"data": {
|
||||
"videos": [
|
||||
{
|
||||
"aweme_id": "v09044g40000cdrtb5jc77u48deft16g",
|
||||
"video_id": "7167433112283008261",
|
||||
"region": "NL",
|
||||
"title": "\u201cYou have been misinformed, as much as we\u2019ve been misinformed.\u201d #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #melanin #africa #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/b9093c120b9944fda3f547001d44a240_1668798075?x-expires=1703124000&x-signature=4%2BExSPK3MmTFdQCMYg72mi5HDzg%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/e124bf8b4c67438b901351498b4b5695_1668798074~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=0bJVu2w0UcX9iAgRQW7h%2F%2F4OYSA%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 40,
|
||||
"play": "https://v19.tiktokcdn-us.com/cce82f9db7d6c71f833e2b53e2abc39b/6582a3a2/video/tos/useast2a/tos-useast2a-pve-0068/okIE3ejh9BGhBnnpUbp4OJQQD71rNdIWeBWARo/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2522&bt=1261&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NGQ1ZTc2aGY5ZTs3PDtoOUBpajRpZzc6Zjh3ZzMzNzczM0A1YDEwXi00Xi0xMjY0YWBgYSMzcWI1cjRnX29gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/0201af034764a80c072969f8d1ebf395/6582a3a2/video/tos/useast2a/tos-useast2a-ve-0068c003/ooYQ9UGW3IjANcl1r8DJBedERBonhQWJnIb9eh/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=2592&bt=1296&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NTM2ZTZpNjZnNjo7NzxlZUBpajRpZzc6Zjh3ZzMzNzczM0AuNl9fYjYtNS8xLTRjMjIuYSMzcWI1cjRnX29gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7167433130218556166.mp3",
|
||||
"music_info": {
|
||||
"id": "7167433094969821958",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7167433130218556166.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 40,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 4059853,
|
||||
"digg_count": 785436,
|
||||
"comment_count": 5817,
|
||||
"share_count": 12996,
|
||||
"download_count": 11150,
|
||||
"create_time": 1668798067,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v0f025gc0000cglvmnrc77u4ni1u20bg",
|
||||
"video_id": "7212008888734878982",
|
||||
"region": "BW",
|
||||
"title": "S/O @Fiksology\ud83e\udef4 ka DC\ud83d\udd25@gavin.ubuntu @Jame McVernando #ubuntubandmerch #thamiubuntu #SAMA28 ",
|
||||
"cover": "https://p16-sign-useast2a.tiktokcdn.com/obj/tos-useast2a-p-0037-aiso/29910b3791a7474babf262da0477c5ee_1680604010?x-expires=1703124000&x-signature=96i6RGuz10acj1SwttTl1FwE9YQ%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-p-0037-aiso/71a9299ab7a446d39f99542fefcf521a_1680604009~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=s%2FD6JYmr8UF3GflvYGTwX%2FhlvVE%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 22,
|
||||
"play": "https://v16m.tiktokcdn-us.com/fbc1b7c9d6df457b01e73329c76b5592/6582a390/video/tos/useast2a/tos-useast2a-pve-0037-aiso/oMAXWfuEQQ8FJFNSeEr89paDofng8LACfAbT3g/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1840&bt=920&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=4&rc=M2dpNjhkZWY0PDhmaTc4Z0BpajN4bDc6ZnF5ajMzZjgzM0AvYjQvMC9fX2AxL2JhX14vYSNfLy5rcjRvamlgLS1kL2Nzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/18d13ec31cad5bffc05a1f795a3769aa/6582a390/video/tos/useast2a/tos-useast2a-pve-0037c001-aiso/o4AgX8Cfar83ne9MWbgEfbJSA8DINxHoAQCfQu/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=4390&bt=2195&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NjNkMzg8NmhkaWg7NDs6ZEBpajN4bDc6ZnF5ajMzZjgzM0BiXzBfX14zXjMxNDMxMjY1YSNfLy5rcjRvamlgLS1kL2Nzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7205224029014264582.mp3",
|
||||
"music_info": {
|
||||
"id": "7205224038348000005",
|
||||
"title": "original sound - basiiey_monnapula",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7205224029014264582.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/ae049960ea6786673783ce66b36dc7da~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=%2FnY5uAyksD41s%2Bm8ECAFLIAtXfo%3D",
|
||||
"author": "Basetsana Monnapula",
|
||||
"original": true,
|
||||
"duration": 32,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 6868872,
|
||||
"digg_count": 673843,
|
||||
"comment_count": 2832,
|
||||
"share_count": 8103,
|
||||
"download_count": 463,
|
||||
"create_time": 1679176676,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "217487781219024896",
|
||||
"unique_id": "thami.ubuntu",
|
||||
"nickname": "Thami Philemon",
|
||||
"avatar": "https://p16-sign-useast2a.tiktokcdn.com/tos-useast2a-avt-0068-giso/e546ddc5cc70ede037ac5397cd068323~c5_300x300.jpeg?x-expires=1703124000&x-signature=gFNFdUl%2BtBIHAI2sX2ANcjUvqyo%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cc2hhdbc77ufp0p13l9g",
|
||||
"video_id": "7135137277650439430",
|
||||
"region": "NL",
|
||||
"title": "That that you do most, will be that that you do best #jawanzakunjufu #NBA #blackthinkers #imhotep #blackhistory #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/09d637f654744d62826facb8a685a44f_1661278613?x-expires=1703124000&x-signature=85qRg1ex%2FkscTT8sSfRpq7XSvos%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/bbc3f2523b50459099bec727131b73c0_1661278613~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=PIvHym7TJAU%2BXGScgIuFGaRVkzI%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 60,
|
||||
"play": "https://v19.tiktokcdn-us.com/1f8f8c36c8e7b1d288c09be54090c378/6582a3b6/video/tos/useast2a/tos-useast2a-ve-0068c002/2a36ec9fd62e44a79fd341682312a92e/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1212&bt=606&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=Zjs1ZjxlNWY8ZzllNTw5ZkBpam80M2k6ZmdrZjMzNzczM0BjNjBfXzA1NWAxLWAxNDMzYSM2MG1tcjRfZS9gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/a48c0af8c7cdf127e4c085c61adfbb89/6582a3b6/video/tos/useast2a/tos-useast2a-pve-0068/7d4538584c7e424a9aa60e034d02c6e8/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1254&bt=627&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NjVkaDxlZ2c0aWllZ2Q1NkBpam80M2k6ZmdrZjMzNzczM0BeLl4uNmA0X2AxMGFfLy80YSM2MG1tcjRfZS9gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7135137305857460998.mp3",
|
||||
"music_info": {
|
||||
"id": "7135137291257154310",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7135137305857460998.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 60,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 3509817,
|
||||
"digg_count": 406909,
|
||||
"comment_count": 5287,
|
||||
"share_count": 27964,
|
||||
"download_count": 12202,
|
||||
"create_time": 1661278609,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbhc463c77u5po59g8fg",
|
||||
"video_id": "7125471227543670022",
|
||||
"region": "NL",
|
||||
"title": "They don\u2019t tell us about ourselves. #blackpeople #africancommunity #blackpower #blackunity #burnaboy #blackhistory #ancientafrica #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/27058259ac9c4f7996ed15f7d35fcb42_1659028058?x-expires=1703124000&x-signature=zt3tBpttXmjohAqGGMuQZU0Ef%2FU%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/5b086bdb9aa644448d1219834f894637_1659028057~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=0%2BVWKeBo8T%2FHZ2bfJx7bGxsnNUo%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 43,
|
||||
"play": "https://v16m.tiktokcdn-us.com/d5828b1ca12fa0904ce987640b5debfc/6582a3a5/video/tos/useast2a/tos-useast2a-ve-0068c003/1dab5ac30d07444fb1115e537e4656fd/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1362&bt=681&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=aDU7aDVpNWdmOGk6NjhoNkBpajs8cjg6ZjlmZTMzNzczM0BeYmEzYi8xXzExYTYvMi1fYSNjZDJtcjQwMWVgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/b8f7853d30e428a6af5bc705ffeb227d/6582a3a5/video/tos/useast2a/tos-useast2a-ve-0068c004/a8726699cd774e08b10c124967f1095c/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1398&bt=699&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ZzNlM2k0OWg2OGhpNWVkaUBpajs8cjg6ZjlmZTMzNzczM0AyLmEwL2IvXi4xY2MxMTAuYSNjZDJtcjQwMWVgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7125471236305849094.mp3",
|
||||
"music_info": {
|
||||
"id": "7125471221475527429",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7125471236305849094.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 43,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 2663740,
|
||||
"digg_count": 289424,
|
||||
"comment_count": 4539,
|
||||
"share_count": 16995,
|
||||
"download_count": 62151,
|
||||
"create_time": 1659028056,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cht15sjc77u6asjve4b0",
|
||||
"video_id": "7240121196505812229",
|
||||
"region": "TR",
|
||||
"title": "#linux #windows #windows10 #windows8 #windows81 #windows7 #windowsvista #windowsxp #microsoft #gnome #macos #apple #arch #ubuntu #comedyvideo #komedi #komik #fyp #kesfet #ke\u015ffet #turkey #turkiye #teknoloji #bili\u015fim #bilisim #pcsystem #2023 #intelcore #bilgisayar #nvidiageforce #gamer #edit #pcbuild #phonk #amdryzen5000series #61 #trabzon ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/912a019391e84924a0feba2bafc5833c_1685722084?x-expires=1703124000&x-signature=L2s%2BqZmvLKnWjgFn1LIQkRqb61M%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/ee011f41126140cea95ed09e2f928eb9_1685722084~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=xo8psLpjw39%2BJiowhbMq8JT5vNs%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 14,
|
||||
"play": "https://v16m.tiktokcdn-us.com/6f76e364f63a124e0cecd737216514a4/6582a389/video/tos/useast2a/tos-useast2a-ve-0068c001/o0cgJIJyAXLIhZSIjksCCuf2eDo6Qg80XrGebn/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3484&bt=1742&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ZDQzaTdmO2Q1MzppNjxmZEBpMzd5djk6ZnY0azMzNzczM0AwYF9eLmI2Ni0xMjNjLTJjYSNfYmdecjRnMnFgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/8466b29aabc35b2e0d1cae354b5bc489/6582a389/video/tos/useast2a/tos-useast2a-pve-0068/oUr6jIDrepkDIQtuCAUQEMLbyfBgqeInQ3SCWh/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3566&bt=1783&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=OztpNzQ1aDNoPDtoOjQ8Z0BpMzd5djk6ZnY0azMzNzczM0AyLTEvLzFiNS0xLjFeMzVjYSNfYmdecjRnMnFgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7240121223304235782.mp3",
|
||||
"music_info": {
|
||||
"id": "7240121203456428805",
|
||||
"title": "original sound - drizzlep6",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7240121223304235782.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/251581f4d1594f04ea2f325ff4cc2a07~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=2LG3zWl2%2B7QD8BPwl4NTwXj2bXw%3D",
|
||||
"author": "Milli G\u00f6r\u00fc\u015f\u00e7\u00fc Volkan \ud83c\udf3f\ud83c\udf41",
|
||||
"original": true,
|
||||
"duration": 15,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 1704069,
|
||||
"digg_count": 247851,
|
||||
"comment_count": 29,
|
||||
"share_count": 40070,
|
||||
"download_count": 491,
|
||||
"create_time": 1685722082,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7214552603395916805",
|
||||
"unique_id": "drizzlep6",
|
||||
"nickname": "Milli G\u00f6r\u00fc\u015f\u00e7\u00fc Volkan \ud83c\udf3f\ud83c\udf41",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/251581f4d1594f04ea2f325ff4cc2a07~c5_300x300.jpeg?x-expires=1703124000&x-signature=VNzWdrg82BH2GPcFrY2f5AOs7OQ%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cghd6ujc77ucrvaksck0",
|
||||
"video_id": "7215562429403565317",
|
||||
"region": "AT",
|
||||
"title": "would be a shame if i forgot to turn of the network #virtualmachine #vm #virtualbox #windows #linux #ubuntu ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/5fe4d7b7b2524275af1b0e245641ff49_1680004049?x-expires=1703124000&x-signature=xjALXpMVrtWAlD4JQx47n3PLCHw%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/c9d3e9b708164fe0b78d35b47bba445b_1680004049~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=aRETt6Wk39aE1F4zOKWceUGQFKY%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 7,
|
||||
"play": "https://v16m.tiktokcdn-us.com/23adf16946df423842b6b0ce4eb05d25/6582a381/video/tos/useast2a/tos-useast2a-pve-0068/okLmMJhUIgoy9xEN90fkidIOo9mVqCA3zAWIDM/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=414&bt=207&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NTtnZWk3ZWU5aTlpZTc5PEBpM2ZueWY6ZnhnajMzNzczM0AzXzFiXzEtXzExYWMuMjM2YSNocF5vcjRnM2VgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/5006f2b9180bc17f23aa2c733bd751a2/6582a381/video/tos/useast2a/tos-useast2a-ve-0068c002/o4NDMgNh4oE9y9ok3UAmIOAaY9IPfVC7zMsW0i/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=442&bt=221&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=NWhlNmdoZTRlZDw0NzVmPEBpM2ZueWY6ZnhnajMzNzczM0A1MjAxMWMzNTAxXjAzXmAuYSNocF5vcjRnM2VgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/94e9293d28a3496981f09994bdb78107",
|
||||
"music_info": {
|
||||
"id": "6855250725862115330",
|
||||
"title": "Coconut Mall (From \"Mario Kart Wii\")",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/94e9293d28a3496981f09994bdb78107",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/a8f7d19a20714d9f9e8adc4e69d0708c.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Mario Kart Wii, The Themes"
|
||||
},
|
||||
"play_count": 1526953,
|
||||
"digg_count": 194938,
|
||||
"comment_count": 408,
|
||||
"share_count": 3334,
|
||||
"download_count": 748,
|
||||
"create_time": 1680004048,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6539155051389129743",
|
||||
"unique_id": "robert_juergerer",
|
||||
"nickname": "Robert J\u00fcrgerer",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/8a0e92e0a08bbab2c6c25a13a73ddb35~c5_300x300.jpeg?x-expires=1703124000&x-signature=fREAm5jTL1lkhWduDuVUiFZsP9o%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000cbmogv3c77ueej864sc0",
|
||||
"video_id": "7128504907803397382",
|
||||
"region": "NL",
|
||||
"title": "A new type of thinking #malcolmx #blackleaders #blackpeople #africancommunity #blackpower #blackunity #panafricanism #fyp #foryou #panafrican #afrikancommunity #blackconsciousness #blackcommunity #unlearnandrelearn #blackexcellence #problack #knowthyself #ubuntu",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/faca00b819ce4e9bad1ec0cb8cbe6d63_1659734391?x-expires=1703124000&x-signature=iuELTcdjUHK1EjDlrDB%2FYRaEdws%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/13d5e51f74a94aaab819798d08c1f86d_1659734391~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=FMv9WMW2COiSjgkJyO437KGKCBU%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 52,
|
||||
"play": "https://v19.tiktokcdn-us.com/5a0e715186b2fbfc8132b11429969df5/6582a3ae/video/tos/useast2a/tos-useast2a-pve-0068/352bd236bd0b4a3a8d2213fa503d0a9a/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1208&bt=604&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=aDNmaTpmZDQ1ZzlkODlnNUBpM3Y5bWg6ZnlyZTMzNzczM0A0MTU0MS4vXmMxMTE1LzQvYSNgMTVicjQwZGpgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"wmplay": "https://v19.tiktokcdn-us.com/76add4e2a195de9c9b2cc8a1fef7b93b/6582a3ae/video/tos/useast2a/tos-useast2a-ve-0068c004/81c10525667444f889c96ca540674e07/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1244&bt=622&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=Nzw1Zjs5OWRoNjU1NmlnZkBpM3Y5bWg6ZnlyZTMzNzczM0BeLzFiXjJjX2ExMzRhMzNjYSNgMTVicjQwZGpgLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00008000",
|
||||
"music": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7128504954595412741.mp3",
|
||||
"music_info": {
|
||||
"id": "7128504937776679686",
|
||||
"title": "original sound - afrikancommunity",
|
||||
"play": "https://sf16-ies-music-va.tiktokcdn.com/obj/musically-maliva-obj/7128504954595412741.mp3",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_1080x1080.jpeg?x-expires=1703124000&x-signature=kP1cVyhAwezOqb3NoY3PVxO9Oyo%3D",
|
||||
"author": "afrikancommunity",
|
||||
"original": true,
|
||||
"duration": 52,
|
||||
"album": ""
|
||||
},
|
||||
"play_count": 1925864,
|
||||
"digg_count": 187364,
|
||||
"comment_count": 4708,
|
||||
"share_count": 22382,
|
||||
"download_count": 24979,
|
||||
"create_time": 1659734390,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": false,
|
||||
"commerce_info": {
|
||||
"adv_promotable": false,
|
||||
"auction_ad_invited": false,
|
||||
"branded_content_type": 0,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "6813642741462287365",
|
||||
"unique_id": "afrikancommunity",
|
||||
"nickname": "afrikancommunity",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/93c33be7e299ea8cf1762b3ff08175bc~c5_300x300.jpeg?x-expires=1703124000&x-signature=QIJZsNfd2YOgDg8lMGEqGQeam24%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
},
|
||||
{
|
||||
"aweme_id": "v09044g40000ck0isu3c77u97chrtho0",
|
||||
"video_id": "7278150686989159685",
|
||||
"region": "ZA",
|
||||
"title": "#music #amapiano #mellowandsleazy #tmanxpress #SAMA28 ",
|
||||
"cover": "https://p16-sign-va.tiktokcdn.com/obj/tos-maliva-p-0068/4214065b3b6f4009b2e4808493fbd2d0_1694576516?x-expires=1703124000&x-signature=QyQVCscBgVlZQ8bodf4uS6htdfc%3D&s=SEARCH&se=false&sh=&sc=dynamic_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"origin_cover": "https://p16-sign-va.tiktokcdn.com/tos-maliva-p-0068/d06fe6a869dc4f869f18a9e1ed0b3887_1694576523~tplv-tiktokx-360p.webp?x-expires=1703124000&x-signature=e7hRfKon9cvBKqt8Y8%2BkxvIwvlg%3D&s=SEARCH&se=false&sh=&sc=feed_cover&l=20231220021905B0B31567CD73EF000289",
|
||||
"duration": 59,
|
||||
"play": "https://v16m.tiktokcdn-us.com/4dbf46e2875addad8d023ad4a99249e7/6582a3b5/video/tos/useast2a/tos-useast2a-ve-0068c003/okMkjuDIQBQl4vAbkXeJCkA9gy5dfEAdnBSRgS/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=1718&bt=859&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=6&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=4&rc=NDxoZmU3Zjc3Njg0aTVlNkBpM2t1Zjw6ZnhsbjMzNzczM0AzYjBiYTEuNmIxNTRhMWAyYSNscWU0cjQwcC1gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"wmplay": "https://v16m.tiktokcdn-us.com/d4b759e76706f51d9400f0c172b11afc/6582a3b5/video/tos/useast2a/tos-useast2a-pve-0068/owIAJI4Endn5SBLxQDb5AZQ7eedkLlBEzSwRHB/?a=1233&ch=0&cr=13&dr=0&lr=all&cd=0%7C0%7C0%7C&cv=1&br=3264&bt=1632&bti=NEBzNTY6QGo6OjZALnAjNDQuYCMxNDNg&cs=0&ds=3&ft=kJrRfy7oZtc0PD1Agptxg9wp6DhXvEeC~&mime_type=video_mp4&qs=0&rc=ODw6PDQ3PDhoOTU1OzppZkBpM2t1Zjw6ZnhsbjMzNzczM0AzLTU0YDQ0XmExMWFeXjFeYSNscWU0cjQwcC1gLS1kMTZzcw%3D%3D&l=20231220021905B0B31567CD73EF000289&btag=e00010000",
|
||||
"music": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o0DzM4MSCXFMRonTCAb3e2FgOYIfy1weDJpJSa",
|
||||
"music_info": {
|
||||
"id": "7264936944639330305",
|
||||
"title": "Imnandi lento (feat. Tman Xpress)",
|
||||
"play": "https://sf16-ies-music-sg.tiktokcdn.com/obj/tos-alisg-ve-2774/o0DzM4MSCXFMRonTCAb3e2FgOYIfy1weDJpJSa",
|
||||
"cover": "https://p16-sg.tiktokcdn.com/aweme/720x720/tos-alisg-v-2774/oEk3DTvnEAJSBQ6tESAczBCoAZetLQbGugfn7D.jpeg",
|
||||
"author": "",
|
||||
"original": false,
|
||||
"duration": 60,
|
||||
"album": "Imnandi lento (feat. Tman Xpress)"
|
||||
},
|
||||
"play_count": 3087192,
|
||||
"digg_count": 182514,
|
||||
"comment_count": 652,
|
||||
"share_count": 11954,
|
||||
"download_count": 38,
|
||||
"create_time": 1694576513,
|
||||
"anchors": null,
|
||||
"anchors_extras": "",
|
||||
"is_ad": true,
|
||||
"commerce_info": {
|
||||
"ad_source": 1,
|
||||
"adv_promotable": true,
|
||||
"auction_ad_invited": false,
|
||||
"bc_label_test_text": "Paid partnership",
|
||||
"branded_content_type": 4,
|
||||
"with_comment_filter_words": false
|
||||
},
|
||||
"commercial_video_info": "",
|
||||
"author": {
|
||||
"id": "7276322982490014725",
|
||||
"unique_id": "ubuntuedits",
|
||||
"nickname": "UBUNTU",
|
||||
"avatar": "https://p16-sign-va.tiktokcdn.com/tos-maliva-avt-0068/e9229e849809078be4bb64b959b4e08d~c5_300x300.jpeg?x-expires=1703124000&x-signature=eRgWDhD7lrp6A7r1IOKMfRggyPY%3D"
|
||||
},
|
||||
"is_top": 0
|
||||
}
|
||||
],
|
||||
"cursor": 10,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "LibreOffice Impress Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "LibreOffice Calc Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "Thunderbird Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,52 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
import socket
|
||||
socket.setdefaulttimeout(500)
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
type="video"
|
||||
).execute()
|
||||
|
||||
videos = []
|
||||
|
||||
for search_result in search_response.get("items", []):
|
||||
if search_result["id"]["kind"] == "youtube#video":
|
||||
video_id = search_result["id"]["videoId"]
|
||||
video_metadata = get_video_metadata(api_key, video_id)
|
||||
videos.append(video_metadata)
|
||||
|
||||
return videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'AIzaSyDI_BBExs-HypVZFxgnR5tj5S6-uKyU4vk' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "Ubuntu Desktop Tutorial", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||
@@ -1,65 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
|
||||
def search_youtube(api_key, query, max_results=50, language="en"):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
videos = []
|
||||
next_page_token = None
|
||||
total_results = 0
|
||||
|
||||
while True:
|
||||
search_response = youtube.search().list(
|
||||
q=query,
|
||||
part="id,snippet",
|
||||
maxResults=max_results,
|
||||
pageToken=next_page_token,
|
||||
type="video",
|
||||
relevanceLanguage=language
|
||||
).execute()
|
||||
|
||||
video_ids = [item['id']['videoId'] for item in search_response.get("items", []) if
|
||||
item['id']['kind'] == 'youtube#video']
|
||||
|
||||
# Fetch metadata for each video
|
||||
videos.extend([get_video_metadata(api_key, video_id) for video_id in video_ids])
|
||||
|
||||
total_results += len(video_ids)
|
||||
next_page_token = search_response.get('nextPageToken')
|
||||
|
||||
if not next_page_token or total_results >= max_results:
|
||||
break
|
||||
|
||||
# Sort videos by view count
|
||||
sorted_videos = sorted(videos, key=lambda x: int(x['items'][0]['statistics']['viewCount']), reverse=True)
|
||||
|
||||
return sorted_videos
|
||||
|
||||
|
||||
def get_video_metadata(api_key, video_id):
|
||||
youtube = build('youtube', 'v3', developerKey=api_key)
|
||||
|
||||
request = youtube.videos().list(
|
||||
part="snippet,contentDetails,statistics",
|
||||
id=video_id
|
||||
)
|
||||
response = request.execute()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
api_key = 'API_KEY' # Replace with your actual API key
|
||||
|
||||
# Search for videos related to "VLC player"
|
||||
vlc_related_videos = search_youtube(api_key, "VLC player", max_results=10)
|
||||
|
||||
# create data folder if not exist
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
for video in vlc_related_videos:
|
||||
# store the video metadata into a json file
|
||||
with open(f"data/{video['etag']}.json", "w") as f:
|
||||
json.dump(video, f, indent=4)
|
||||