diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e3c5c78 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76a388e --- /dev/null +++ b/.gitignore @@ -0,0 +1,159 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyc +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +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 + +*.wwebjs_cache + +# 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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__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 maintainted 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/ +*.pyc + +.idea +/node_modules +/frontend diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e550b32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 daxcay + +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. diff --git a/README.md b/README.md index 41f9a31..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,2 +0,0 @@ -# ComfyUI-YT -Play youtube videos inside comfyui diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b6b2506 --- /dev/null +++ b/__init__.py @@ -0,0 +1,29 @@ +# By Jerry Davos & Daxton Caylor +# +# 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. + +""" +@author: Daxton Caylor & Jerry Davos +@title: ComfyUI-YouTubeVideoPlayer +@nickname: ComfyUI-YouTubeVideoPlayer +@description: YouTube Video Player in Comfy. +""" + +from .classes.YouTubeVideoPlayer import N_CLASS_MAPPINGS as YouTubeVideoPlayerMappings, N_DISPLAY_NAME_MAPPINGS as YouTubeVideoPlayerNameMappings + +NODE_CLASS_MAPPINGS = {} +NODE_CLASS_MAPPINGS.update(YouTubeVideoPlayerMappings) + +NODE_DISPLAY_NAME_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS.update(YouTubeVideoPlayerNameMappings) + +WEB_DIRECTORY = "./web" \ No newline at end of file diff --git a/classes/YouTubeVideoPlayer.py b/classes/YouTubeVideoPlayer.py new file mode 100644 index 0000000..14cd3ac --- /dev/null +++ b/classes/YouTubeVideoPlayer.py @@ -0,0 +1,27 @@ +class YouTubeVideoPlayer: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": {}, + "optional": {}, + "hidden": {} + } + + FUNCTION = "doit" + RETURN_TYPES = () + OUTPUT_NODE = True + + def doit(self): + return () + +N_CLASS_MAPPINGS = { + "YouTubeVideoPlayer": YouTubeVideoPlayer, +} + +N_DISPLAY_NAME_MAPPINGS = { + "YouTubeVideoPlayer": "YouTubeVideoPlayer", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..edd303b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "comfyui-youtubevideoplayer" +description = "youtube video player in comfyui" +version = "1.0.0" +license = { file = "LICENSE" } +dependencies = [] + +[project.urls] +Repository = "https://github.com/daxcay/ComfyUI-YouTubeVideoPlayer" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "daxcay" +DisplayName = "ComfyUI-YouTubeVideoPlayer" +Icon = "https://raw.githubusercontent.com/daxcay/ComfyUI-YouTubeVideoPlayer/main/web/assets/img/logo.png" \ No newline at end of file diff --git a/web/assets/logo.png b/web/assets/logo.png new file mode 100644 index 0000000..3f5dc88 Binary files /dev/null and b/web/assets/logo.png differ diff --git a/web/util.js b/web/util.js new file mode 100644 index 0000000..c9ac6b0 --- /dev/null +++ b/web/util.js @@ -0,0 +1,171 @@ +import { app } from "../../scripts/app.js"; + +class ComfyUIYouTubeVideoPlayer { + + constructor(app) { + + this.app = app + this.nodes = [] + this.elements = {} + this.onNodeAdded = this.onNodeAdded.bind(this); + this.onNodeRemoved = this.onNodeRemoved.bind(this); + this.onInputChanged = this.onInputChanged.bind(this); + + } + + onNodeAdded(node) { + if (node.type === "YouTubePlayer") { + this.registerNode(node, 1) + } + } + + onNodeRemoved(node) { + if (node.type === "YouTubePlayer") { + this.registerNode(node, 0) + } + } + + defineElements(node) { + + let node_id = node.id + + this.elements[node_id] = {} + this.elements[node_id].node = node + this.elements[node_id].iframe = this.createYouTubeIframe(`ytp_${node_id}_frame`) + this.elements[node_id].input = this.createInput(`ytp_${node_id}_input`) + + let input = node.addDOMWidget("textbox", `ytp_${node_id}_input`, this.elements[node_id].input, { + serialize: false, + hideOnZoom: false + }); + + let sp1 = node.addDOMWidget("iframe", `ytp_${node_id}_spacer`, document.createElement('div'), { + serialize: false, + hideOnZoom: false + }); + + node.addDOMWidget("iframe", `ytp_${node_id}_frame`, this.elements[node_id].iframe, { + serialize: false, + hideOnZoom: false + }); + + input.computeSize = function (width) { + this.computeHeight = 30 + return [width, 30] + } + + sp1.computeSize = function (width) { + this.computeHeight = 5 + return [width, 5] + } + + node.setSize([640, 480]) + + this.elements[node_id].input.addEventListener('input', this.onInputChanged.bind(this, node_id)) + + } + + onInputChanged(node_id, event) { + let video_id = this.getYouTubeVideoId(event.target.value) + if (video_id && this.elements[node_id]) { + this.elements[node_id].node.setProperty("url", video_id) + this.updateIframeSRC(this.elements[node_id].iframe, video_id) + } + } + + registerNode(node, mode) { + if (mode == 1) { + if (!this.nodes.includes(node)) { + this.nodes.push(node) + this.defineElements(node) + if (node.properties['url'] && node.properties['url'].length > 0) { + this.updateIframeSRC(this.elements[node.id].iframe, node.properties['url']) + this.elements[node.id].input.value = `https://www.youtube.com/embed/${node.properties['url']}` + } + } + else { + if (this.elements[node.id] && node.properties['url'] && node.properties['url'].length > 0) { + this.first_time = true + this.updateIframeSRC(this.elements[node.id].iframe, node.properties['url']) + this.elements[node.id].input.value = `https://www.youtube.com/embed/${node.properties['url']}` + } + } + } + else if (mode == 0) { + if (this.nodes.includes(node)) { + this.nodes.splice(this.nodes.indexOf(node), 1) + if (this.elements[node.id]) { + delete this.elements[node.id] + } + } + } + } + + registerNodes() { + this.app.graph._nodes.forEach(node => { + if (node.type === "YouTubePlayer") { + this.registerNode(node, 1) + } + }) + } + + createInput(nodeid) { + var input = document.createElement('input'); + input.type = "text" + input.id = nodeid + input.placeholder = "Enter youtube url: " + input.style.border = "none" + input.style.textIndent = "10px" + input.style.borderRadius = "4px" + input.style.backgroundColor = 'rgb(40,40,40)' + return input; + } + + updateIframeSRC(iframe, videoId) { + iframe.src = `https://www.youtube.com/embed/${videoId}`; + } + + createYouTubeIframe(nodeid) { + var iframe = document.createElement('iframe'); + iframe.id = nodeid; + iframe.frameBorder = '0'; + iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; + iframe.allowFullscreen = true; + iframe.style.borderRadius = "5px" + iframe.style.backgroundColor = 'black' + return iframe; + } + + getYouTubeVideoId(url) { + var videoId = null; + var standardPattern = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/.*[?&]v=([^&]+)/; + var shortPattern = /(?:https?:\/\/)?(?:www\.)?youtu\.be\/([^?&]+)/; + var match = url.match(standardPattern); + if (match && match[1]) { + videoId = match[1]; + } else { + match = url.match(shortPattern); + if (match && match[1]) { + videoId = match[1]; + } + } + return videoId; + } + +} + +let youtube = new ComfyUIYouTubeVideoPlayer(app) + +let youtubeExt = { + name: "ComfyUIYoutube", + async setup() { + youtube.app.graph.onNodeAdded = youtube.onNodeAdded.bind(this); + youtube.app.graph.onNodeRemoved = youtube.onNodeRemoved.bind(this); + }, + async afterConfigureGraph() { + console.log("YouTubePlayer Extension Loaded"); + youtube.registerNodes() + } +}; + +app.registerExtension(youtubeExt);