├── app ├── tests │ ├── __init__.py │ └── test_app.py ├── __init__.py ├── static │ ├── example_file │ │ ├── broken_heart.png │ │ ├── simple.csv │ │ ├── example.csv │ │ ├── csv_test_doc_qlab_4.csv │ │ ├── csv_test_doc.csv │ │ └── example_all.csv │ ├── images │ │ └── funny-success-quote-1-picture-quote-1.jpg │ ├── styles │ │ └── styles.css │ └── js │ │ └── index.js ├── helper.py ├── application.py ├── osc_server.py ├── error_success_handler.py ├── templates │ ├── errors.html │ ├── success.html │ └── index.html ├── csv_parser.py ├── cli.py ├── generate_column_docs.py └── osc_config.py ├── website ├── static │ ├── .nojekyll │ ├── CNAME │ ├── img │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── tutorial │ │ │ ├── app-screenshot.png │ │ │ ├── localeDropdown.png │ │ │ ├── eject-screenshot.png │ │ │ ├── app-screenshot-ud.png │ │ │ ├── docsVersionDropdown.png │ │ │ ├── license-screenshot.png │ │ │ ├── move-app-screenshot.png │ │ │ ├── security-message-1.png │ │ │ ├── security-message-3.png │ │ │ └── open-anyway-screenshot.png │ │ ├── funny-success-quote-1-picture-quote-1.jpg │ │ ├── qlab-icon.svg │ │ ├── cartoon-rocket.svg │ │ └── pink-cake.svg │ └── js │ │ └── brevo.js ├── docs │ ├── advanced │ │ └── _category_.json │ ├── tutorial-basics │ │ ├── _category_.json │ │ ├── installation.mdx │ │ ├── send-to-qlab.md │ │ ├── cli-advanced.md │ │ └── prepare-csv-file.md │ ├── reference │ │ ├── _category_.json │ │ └── csv-columns.md │ ├── developer │ │ ├── _category_.json │ │ ├── building-releases.md │ │ ├── architecture.md │ │ ├── adding-properties.md │ │ └── testing.md │ └── intro.md ├── babel.config.js ├── src │ ├── components │ │ ├── HomepageFeatures.module.css │ │ ├── DropdownButton.js │ │ ├── HomepageFeatures.js │ │ └── StatusBadges.js │ ├── pages │ │ ├── index.module.css │ │ └── index.js │ └── css │ │ └── custom.css ├── .gitignore ├── releases │ ├── 2021-02-26-v2021.1.1.mdx │ ├── 2020-11-12-First-Release.mdx │ ├── 2023-7-9-v2023.1.mdx │ ├── 2022-09-01-v2022.3.mdx │ ├── 2023-10-14-v2023.3.mdx │ ├── 2021-12-1-v2021.1.2.mdx │ ├── 2022-08-29-v2022.2.mdx │ ├── 2021-02-25-v2021.1.0.mdx │ ├── 2023-12-20-v2023.5.mdx │ ├── 2021-11-24-v2021.1.15.mdx │ ├── 2024-8-6-v2024.2.mdx │ ├── 2024-3-30-v2024.1.mdx │ ├── 2023-12-18-v2023.4.mdx │ ├── 2023-7-14-v2023.2.mdx │ └── 2025-10-3-v2025.1.mdx ├── README.md ├── sidebars.js ├── package.json └── docusaurus.config.js ├── .coverage ├── icon.icns ├── requirements.txt ├── CLAUDE.md ├── .gitignore ├── run_gui.py ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── codeql-analysis.yml │ ├── documentation.yml │ ├── release.yml │ └── pytest.yml ├── .spellcheck.yml ├── .wordlist.txt ├── application.spec ├── setup.py └── README.md /app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/CNAME: -------------------------------------------------------------------------------- 1 | csv-to-qlab.finlayross.me -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | """CSV to QLab application package""" 2 | -------------------------------------------------------------------------------- /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/.coverage -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/icon.icns -------------------------------------------------------------------------------- /website/docs/advanced/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced Installation", 3 | "position": 3 4 | } -------------------------------------------------------------------------------- /website/docs/tutorial-basics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Basics", 3 | "position": 2 4 | } 5 | -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.3 2 | python-osc==1.8.3 3 | pywebview==5.1 4 | pytest>=8.0.0 5 | pytest-cov>=4.1.0 6 | -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | - when running python commands make sure you are in the virtual environment by running: source env/bin/activate -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /app/static/example_file/broken_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/app/static/example_file/broken_heart.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .DS_Store 3 | env 4 | dist 5 | dist-2 6 | build 7 | icon.svg 8 | icon(big).icns 9 | .pytest_cache 10 | .vscode -------------------------------------------------------------------------------- /website/static/img/tutorial/app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/app-screenshot.png -------------------------------------------------------------------------------- /website/static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /website/static/img/tutorial/eject-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/eject-screenshot.png -------------------------------------------------------------------------------- /website/static/img/tutorial/app-screenshot-ud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/app-screenshot-ud.png -------------------------------------------------------------------------------- /website/static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /website/static/img/tutorial/license-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/license-screenshot.png -------------------------------------------------------------------------------- /website/static/img/tutorial/move-app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/move-app-screenshot.png -------------------------------------------------------------------------------- /website/static/img/tutorial/security-message-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/security-message-1.png -------------------------------------------------------------------------------- /website/static/img/tutorial/security-message-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/security-message-3.png -------------------------------------------------------------------------------- /website/static/img/tutorial/open-anyway-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/tutorial/open-anyway-screenshot.png -------------------------------------------------------------------------------- /app/static/images/funny-success-quote-1-picture-quote-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/app/static/images/funny-success-quote-1-picture-quote-1.jpg -------------------------------------------------------------------------------- /website/static/img/funny-success-quote-1-picture-quote-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fross123/csv_to_qlab/HEAD/website/static/img/funny-success-quote-1-picture-quote-1.jpg -------------------------------------------------------------------------------- /website/docs/reference/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Reference", 3 | "position": 5, 4 | "description": "Complete reference documentation for CSV columns and configuration" 5 | } 6 | -------------------------------------------------------------------------------- /website/docs/developer/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Developer Documentation", 3 | "position": 4, 4 | "description": "Architecture and development guides for contributing to CSV to QLab" 5 | } 6 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /run_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Entry point for PyInstaller GUI builds""" 3 | from app.application import app 4 | import webview 5 | 6 | if __name__ == "__main__": 7 | webview.create_window("CSV to QLab", app, frameless=True, width=300, height=465) 8 | webview.start() 9 | -------------------------------------------------------------------------------- /app/helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def resource_path(relative_path): 6 | """ 7 | Get absolute path to resource, works for dev and for PyInstaller 8 | """ 9 | base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) 10 | return os.path.join(base_path, relative_path) 11 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /app/static/styles/styles.css: -------------------------------------------------------------------------------- 1 | .btn-primary { 2 | background-color: #f276a0; 3 | border-color: #f276a0; 4 | } 5 | 6 | .btn-primary:hover { 7 | background-color: #f597b7; 8 | border-color: #f597b7; 9 | } 10 | 11 | .custom-control-input:checked~.custom-control-label::before { 12 | background-color: #f597b7; 13 | border-color: #f597b7; 14 | } -------------------------------------------------------------------------------- /website/static/img/qlab-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/static/js/brevo.js: -------------------------------------------------------------------------------- 1 | (function(d, w, c) { 2 | w.BrevoConversationsID = '64b6bf4e19792c25ca0c68bf'; 3 | w[c] = w[c] || function() { 4 | (w[c].q = w[c].q || []).push(arguments); 5 | }; 6 | var s = d.createElement('script'); 7 | s.async = true; 8 | s.src = 'https://conversations-widget.brevo.com/brevo-conversations.js'; 9 | if (d.head) d.head.appendChild(s); 10 | })(document, window, 'BrevoConversations'); -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /website/releases/2021-02-26-v2021.1.1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2021/1/1 3 | title: Version 2021.1.1 4 | tags: ["2021", "1.1"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | - Bug Fixes 12 | - Add in follow options 13 | 14 | 15 |
16 | 19 | Download Release v2021.1.1 20 | 21 |
-------------------------------------------------------------------------------- /website/releases/2020-11-12-First-Release.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 0/0/0 3 | title: First Release 4 | tags: [v0.0.0] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | Initial Release. Download zip to demo. 13 | 14 | Currently only available for MacOS. 15 | 16 |
17 | 20 | Download Release v0.0.0 21 | 22 |
-------------------------------------------------------------------------------- /website/releases/2023-7-9-v2023.1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2023/1 3 | title: Version 2023.1 4 | tags: ["2023", "1"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | - Update application to be backwards compatible and work on more operating systems. 13 | 14 |
15 | 18 | Download Release v2023.1 19 | 20 |
-------------------------------------------------------------------------------- /website/releases/2022-09-01-v2022.3.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2022/9/1 3 | title: Version 2022.3 4 | tags: ["2022", "3"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | * Add feature: passcode use for QLab 5 13 | 14 | Turn on switch and enter passcode from qlab5 settings. 15 | 16 |
17 | 20 | Download Release v2022.2 21 | 22 |
-------------------------------------------------------------------------------- /website/releases/2023-10-14-v2023.3.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2023/3 3 | title: Version 2023.3 4 | tags: ["2023", "3"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | - For text cues, add header of "text" to csv file for text cue content. 13 | - Various updates and bug fixes. 14 | 15 |
16 | 19 | Download Release v2023.3 20 | 21 |
-------------------------------------------------------------------------------- /website/releases/2021-12-1-v2021.1.2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2021/1/2 3 | title: Version 2021.1.2 4 | tags: ["2021", "1.2"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | * Updates User Interface to match theme colors. 13 | 14 | * Removes unnecessary content on app and reduces sizing. 15 | 16 |
17 | 20 | Download Release v2021.1.2 21 | 22 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /website/releases/2022-08-29-v2022.2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2022/8/29 3 | title: Version 2022.2 4 | tags: ["2022", "2"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | * Add feature: "file target" is now an option for audio cues. 13 | 14 | To use, add "file target" as a header. Files can be referenced per [qlab docs](https://qlab.app/docs/v4/scripting/osc-dictionary-v4/#cuecue_numberfiletarget-string). 15 | 16 |
17 | 20 | Download Release v2022.2 21 | 22 |
-------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /.spellcheck.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: html and Markdown 3 | aspell: 4 | lang: en 5 | dictionary: 6 | wordlists: 7 | - .wordlist.txt 8 | encoding: utf-8 9 | pipeline: 10 | - pyspelling.filters.markdown: 11 | - pyspelling.filters.html: 12 | comments: true 13 | attributes: 14 | - title 15 | - alt 16 | ignores: 17 | - :matches(code, pre) 18 | - a:matches(.magiclink-compare, .magiclink-commit) 19 | - span.keys 20 | - :matches(.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon) 21 | sources: 22 | - "website/blog/**/*.html" 23 | - "website/blog/**/*.md" 24 | - "website/blog/**/*.mdx" 25 | - "website/releases/**/*.html" 26 | - "website/releases/**/*.md" 27 | - "website/releases/**/*.mdx" 28 | - "website/docs/**/*.mdx" 29 | - "website/docs/**/*.md" 30 | default_encoding: utf-8 -------------------------------------------------------------------------------- /website/releases/2021-02-25-v2021.1.0.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2021/1/0 3 | title: Version 2021.1.0 4 | tags: ["2021", "1.0"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | - More options can be pulled from the csv file. See README.md for more information 13 | - Updated function to use header:value dictionary so users do not need to be consistent with their spreadsheets 14 | empty cells in csv no longer cause errors 15 | - OSC messages are now sent as a bundle per-cue. 16 | - Names in application.spec changed to csv-to-qlab 17 | 18 |
19 | 22 | Download Release v2021.1.0 23 | 24 |
-------------------------------------------------------------------------------- /website/releases/2023-12-20-v2023.5.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2023/5 3 | title: Version 2023.5 4 | tags: ["2023", "5"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | Bug Fixes: 12 | - Stop Target When Done option will now only check the box when "true". "false" or empty cell will be ignored. Issue #92 13 | - All color selections may not have been sent to QLab. Current colors as of 12/2023 are validated. Issue #91 14 | - Clear errors on consecutive run. In some cases errors continued to be displayed on second run of CSV-To-QLab 15 | 16 | Various other minor updates and fixes. 17 | 18 | 19 |
20 | 23 | Download Release v2023.5 24 | 25 |
-------------------------------------------------------------------------------- /website/releases/2021-11-24-v2021.1.15.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2021/1/15 3 | title: Version 2021.1.15 4 | tags: ["2021", "1.15"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | * Various Bug Fixes 13 | 14 | * Update to OSC handling to allow raw string for standard OSC messages 15 | 16 | * Update to allow "Number Prefix" column to prefix the number of cues. 17 | Ex: If you have a column already with the number of the cue, you may now add another column with "LX", and the numbers of your cues will start with LX, but will still trigger the number selected. The MIDI or OSC command will not include this prefix. 18 | 19 | 20 |
21 | 24 | Download Release v2021.1.15 25 | 26 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /website/releases/2024-8-6-v2024.2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2024/2 3 | title: Version 2024.2 4 | tags: ["2024", "2"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | import DropdownButton from '@site/src/components/DropdownButton.js'; 11 | 12 | export const options = [ 13 | { label: 'macOS 14 (Latest)', value: 'macos13', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab.dmg' }, 14 | { label: 'macOS 13', value: 'macos11', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab-macos13.dmg' }, 15 | { label: 'macOS 12', value: 'macos12', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab-macos12.dmg' }, 16 | ]; 17 | export const buttonClassName = "button button--primary button--lg" 18 | 19 | Bug fix for an error with "follow" vs "Continue Mode". 20 | 21 | Various other minor updates and fixes. 22 | 23 | -------------------------------------------------------------------------------- /website/releases/2024-3-30-v2024.1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2024/1 3 | title: Version 2024.1 4 | tags: ["2024", "1"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | import DropdownButton from '@site/src/components/DropdownButton.js'; 11 | 12 | export const options = [ 13 | { label: 'macOS 14 (Latest)', value: 'macos13', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab.dmg' }, 14 | { label: 'macOS 12', value: 'macos12', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab-macos12.dmg' }, 15 | { label: 'macOS 11', value: 'macos11', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2024.1/CSV-To-QLab-macos11.dmg' }, 16 | ]; 17 | export const buttonClassName = "button button--primary button--lg" 18 | 19 | Features Added: 20 | - New option to adjust video opacity of fade cues. 21 | 22 | Various other minor updates and fixes. 23 | 24 | -------------------------------------------------------------------------------- /website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Welcome! 6 | 7 | I have been working on a solution to a problem that arises constantly when utilizing the popular software QLab to trigger lights, sound, and projections from the same workspace. 8 | 9 | To be able to facilitate these triggers, you usually need to put all of the MIDI and/or OSC cues in the workspace manually. The problem with this is that it is unnecessarily time consuming. You are creating a list that has already been created in your light board or in a spreadsheet, and since they are only “trigger” cues, they do not have the same subtle differences required of sound or video cues. It boils down to pretty much, “LX 12 GO”, “PQ 122 GO”. 10 | 11 | As a solution, I wrote this application that takes a csv spreadsheet file and imports that “trigger” data rapidly and accurately. 12 | 13 | :::note 14 | Keep in mind this project is still in development and may have bugs or issues. 15 | ::: 16 | 17 | :::tip 18 | I recommend testing on an empty QLab workspace and copying the created cues into your working QLab workspace to avoid potential issues. 19 | ::: -------------------------------------------------------------------------------- /.wordlist.txt: -------------------------------------------------------------------------------- 1 | csv 2 | passcode 3 | qlab 4 | QLab 5 | QLab's 6 | 7 | CLI 8 | cli 9 | JSON 10 | json 11 | OSC 12 | osc 13 | UI 14 | 15 | async 16 | Async 17 | Backend 18 | bool 19 | cd 20 | codebase 21 | config 22 | const 23 | continueMode 24 | cov 25 | distributable 26 | doopacity 27 | enums 28 | env 29 | fadeopacity 30 | fi 31 | Frameless 32 | fswatch 33 | hardcoded 34 | inotifywait 35 | IPv 36 | jq 37 | localhost 38 | MockFileStorage 39 | msg 40 | mv 41 | ne 42 | newproperty 43 | OSCConfig 44 | px 45 | py 46 | pyinstaller 47 | PyInstaller 48 | pytest 49 | rf 50 | txt 51 | udp 52 | UDP 53 | UDPClient 54 | unicode 55 | unittest 56 | 57 | css 58 | dmg 59 | js 60 | md 61 | mov 62 | wav 63 | 64 | buttonClassName 65 | Docusaurus 66 | docusaurus 67 | dropdown 68 | DropdownButton 69 | PyWebView 70 | useBaseUrl 71 | 72 | fross 73 | github 74 | GitHub 75 | https 76 | MyDisk 77 | README 78 | src 79 | 80 | CHANGELOG 81 | cuecue 82 | LX 83 | macos 84 | macOS 85 | MacOS 86 | MSC 87 | numbercolorname 88 | numbermode 89 | numbermessagetype 90 | numberqlabcommand 91 | PQ 92 | pre 93 | SysEx 94 | TestDuplicatePropertyNames 95 | TestNewFeature 96 | unpatch 97 | 98 | Hotfix 99 | intra -------------------------------------------------------------------------------- /website/releases/2023-12-18-v2023.4.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2023/4 3 | title: Version 2023.4 4 | tags: ["2023", "4"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | Bug Fixes: 13 | - Fixed an error with setting "File Path" header. 14 | 15 | New Features: 16 | - "Stop Target When Done" for fade cues. Either true or false. 17 | - "Video Stage" number. 18 | - If number is given, set the video stage of the specified cue to that stage. If number is 0, unpatch the specified cue. If number is greater than the number of video stages in the workspace, this message has no effect. number must be a whole number. 19 | 20 | Known Issues: 21 | - Non-standard colors appear to not be accepted by qlab as of this latest release. 22 | :::note 23 | Fixed in [v.2023.5](2023-12-20-v2023.5.mdx) 24 | ::: 25 | 26 |
27 | 30 | Download Release v2023.4 31 | 32 |
-------------------------------------------------------------------------------- /website/releases/2023-7-14-v2023.2.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2023/2 3 | title: Version 2023.2 4 | tags: ["2023", "2"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | 11 | 12 | - Minor updates to row headers. 13 | - Error handling. Response from QLab will be displayed to the user. 14 | - Users now choose which version of QLab they are using. 15 | - Added a generic "Passcode" option for QLab work spaces that are secured by a pin. 16 | - Network cue updates for QLab 5. 17 | - QLab 5 changed network cues significantly. The best use of CSV to QLab is to still do custom OSC commands rather than utilizing the new features. 18 | - Another option is to use CSV to QLab to import the cues, but then edit the network patch for all the network cues that are added. 19 | - For example, if you just need to trigger "GO" to another QLab machine and do not necessarily need to send a cue number in the OSC message. 20 | 21 | 22 |
23 | 26 | Download Release v2023.2 27 | 28 |
-------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv_to_qlab", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "^3.9.1", 18 | "@docusaurus/plugin-google-analytics": "^3.4.0", 19 | "@docusaurus/preset-classic": "^3.9.1", 20 | "@mdx-js/react": "^3.0.1", 21 | "@svgr/webpack": "^8.1.0", 22 | "clsx": "^2.1.1", 23 | "file-loader": "^6.2.0", 24 | "prism-react-renderer": "^2.1.0", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-select": "^5.8.0", 28 | "url-loader": "^4.1.1" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.5%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "engines": { 43 | "node": ">=18.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #f276a0; 10 | --ifm-color-primary-dark: #ef5589; 11 | --ifm-color-primary-darker: #ed457e; 12 | --ifm-color-primary-darkest: #e6165c; 13 | --ifm-color-primary-light: #f597b7; 14 | --ifm-color-primary-lighter: #f7a7c2; 15 | --ifm-color-primary-lightest: #fbd9e4; 16 | --ifm-code-font-size: 95%; 17 | } 18 | 19 | .docusaurus-highlight-code-line { 20 | background-color: rgba(0, 0, 0, 0.1); 21 | display: block; 22 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 23 | padding: 0 var(--ifm-pre-padding); 24 | } 25 | 26 | html[data-theme='dark'] .docusaurus-highlight-code-line { 27 | background-color: rgba(0, 0, 0, 0.3); 28 | } 29 | 30 | .button { 31 | margin: 10px; 32 | } 33 | 34 | .download-select-button { 35 | color: black; 36 | } 37 | 38 | @media (min-width: 650px) { 39 | .download-select-button { 40 | padding-left: 5em; 41 | padding-right: 5em; 42 | border-radius: 10px; 43 | margin-left: 30%; 44 | margin-right: 30%; 45 | width: 40%; 46 | } 47 | } 48 | 49 | @media (max-width: 650px) { 50 | .button { 51 | width: 100%; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /website/src/components/DropdownButton.js: -------------------------------------------------------------------------------- 1 | // DropdownButton.js 2 | 3 | import React, { useState } from 'react'; 4 | import Link from '@docusaurus/Link'; 5 | import Select from 'react-select'; 6 | import styles from './../pages/index.module.css'; 7 | 8 | 9 | const DropdownButton = ({buttonClassName, options }) => { 10 | const [selectedOption, setSelectedOption] = useState(options[0].value); 11 | 12 | const handleDropdownChange = (e) => { 13 | setSelectedOption(e.value); 14 | }; 15 | 16 | const getButtonLink = () => { 17 | const selectedOptionData = options.find((opt) => opt.value === selectedOption); 18 | return selectedOptionData ? selectedOptionData.link : '#'; 19 | }; 20 | 21 | return ( 22 |
23 | 37 | 38 |
39 | 42 | Download 43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default DropdownButton; 50 | -------------------------------------------------------------------------------- /app/static/example_file/simple.csv: -------------------------------------------------------------------------------- 1 | Number,Type,Name 2 | 1,text,house and works 3 | 5,midi,Sound Start 4 | 5.1,midi,house and works 5 | 10,midi,Preshow 6 | 15,midi,House Half 7 | 20,midi,House out 8 | 21,midi,Stage Up 9 | 30,midi,more up in well 10 | 35,midi,add more upstairs 11 | 40,midi,well out 12 | 45,midi,well up 13 | 50,midi,well out 14 | 55,midi,well up 15 | 60,midi,well out 16 | 65,midi,pull down upstairs 17 | 70,midi,blackout 18 | 75,midi,INTERMISSION 19 | 80,midi,House Half 20 | 85,midi,House out 21 | 90,midi,Stage down 22 | 95,midi,Up on Tim 23 | 96,midi,Stage Up for scene light 24 | 100,midi,add light on stairs/second level 25 | 105,midi,lights up “onstage” 26 | 110,midi,lights change onstage 27 | 115,midi,wrong lighting cue 28 | 116,midi,restore back to correct light cue 29 | 120,midi,lights flicker 30 | 125,midi,lights flicker twice 31 | 130,midi,backstage lights snap off 32 | 135,midi,“Stage” lights snap off 33 | 140,midi,INTERMISSION 34 | 145,midi,House Half 35 | 150,midi,House out 36 | 155,midi,Stage Out 37 | 156,midi,False start / stage up 38 | 157,midi,blackout 39 | 160,midi,Stage Up; Tim in “spotlight” 40 | 165,midi,“follow spot” follows Tim 41 | 166,midi,“ 42 | 167,midi,“ 43 | 168,midi,“ 44 | 169,midi,“ 45 | 170,midi,spot out 46 | 175,midi,First look of “Nothing On” 47 | 180,midi,add more upstairs 48 | 185,midi,lights flicker twice (?) 49 | 190,midi,lights flicker (biggest) 50 | 193,midi,smoke from fireplace 51 | 195,midi,quick blackout 52 | 200,midi,stage up for curtain call 53 | 205,midi,bow lights? 54 | 210,midi,stage down 55 | 215,midi,House Up 56 | 220,midi,house and works -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '32 3 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript', 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | 46 | - name: Autobuild 47 | uses: github/codeql-action/autobuild@v1 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v1 51 | -------------------------------------------------------------------------------- /application.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['run_gui.py'], 7 | pathex=['.'], 8 | binaries=[], 9 | datas=[ 10 | ('app/static', 'static'), 11 | ('app/templates', 'templates'), 12 | ('app/qlab_osc_config.json', '.'), 13 | ], 14 | hiddenimports=[], 15 | hookspath=[], 16 | runtime_hooks=[], 17 | excludes=[], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher, 21 | noarchive=False) 22 | pyz = PYZ(a.pure, a.zipped_data, 23 | cipher=block_cipher) 24 | exe = EXE(pyz, 25 | a.scripts, 26 | [], 27 | exclude_binaries=True, 28 | name='csv-to-qlab', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=True, 33 | console=False ) 34 | coll = COLLECT(exe, 35 | a.binaries, 36 | a.zipfiles, 37 | a.datas, 38 | strip=False, 39 | upx=True, 40 | upx_exclude=[], 41 | name='csv-to-qlab') 42 | app = BUNDLE(coll, 43 | name='csv-to-qlab.app', 44 | icon='icon.icns', 45 | bundle_identifier=None, 46 | info_plist={ 47 | 'NSPrincipalClass': 'NSApplication', 48 | 'NSAppleScriptEnabled': False, 49 | 'CFBundleDocumentTypes': [ 50 | { 51 | 'CFBundleTypeName': 'CSV to QLab', 52 | 'CFBundleTypeIconFile': 'icon.icns', 53 | 'LSHandlerRank': 'Owner' 54 | } 55 | ] 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /app/static/example_file/example.csv: -------------------------------------------------------------------------------- 1 | Type,Number,Name,Notes,continueMode,Target,File Target,Color,MIDI Message Type,Midi Command Format,Midi Command,Midi Device ID,Midi Control Number,Midi Control Value,Midi Patch Name,Midi Patch Number,Midi Q List,Midi Q Number,Network Patch Number,Custom String 2 | audio,1,Test 1,,1,,,red,,,,,,,,,,,, 3 | mic,5,Test 2,,2,,,orange,,,,,,,,,,,, 4 | video,9,Test 3,,3,,,green,,,,,,,,,,,, 5 | camera,13,Test 4,,1,,,blue,,,,,,,,,,,, 6 | text,17,Test 5,,2,,,purple,,,,,,,,,,,, 7 | light,21,Test 6,,3,,,red,,,,,,,,,,,, 8 | fade,25,Test 7,,2,,,orange,,,,,,,,,,,, 9 | network,29,Test 8,,3,,,green,,,,,,,,,,,, 10 | midi,33,Test 9,,1,,,blue,,,,,,,,,,,, 11 | midi file,37,Test 10,,2,,,purple,,,,,,,,,,,, 12 | timecode,41,Test 11,,3,,,red,,,,,,,,,,,, 13 | group,45,Test 12,,2,,,orange,,,,,,,,,,,, 14 | start,49,Test 13,,3,5,,green,,,,,,,,,,,, 15 | stop,53,Test 14,,1,5,,blue,,,,,,,,,,,, 16 | pause,57,Test 15,,2,5,,purple,,,,,,,,,,,, 17 | load,61,Test 16,,3,5,,red,,,,,,,,,,,, 18 | reset,65,Test 17,,2,5,,orange,,,,,,,,,,,, 19 | devamp,69,Test 18,,3,9,,green,,,,,,,,,,,, 20 | goto,73,Test 19,,1,5,,blue,,,,,,,,,,,, 21 | target,77,Test 20,,2,57,,purple,,,,,,,,,,,, 22 | arm,81,Test 21,,3,5,,red,,,,,,,,,,,, 23 | disarm,85,Test 22,,2,5,,orange,,,,,,,,,,,, 24 | wait,89,Test 23,,3,,,green,,,,,,,,,,,, 25 | memo,93,Test 24,,0,,,blue,,,,,,,,,,,, 26 | script,97,Test 25,,0,,,purple,,,,,,,,,,,, 27 | list,101,Test 26,,,,,red,,,,,,,,,,,, 28 | cuelist,105,Test 27,,,,,orange,,,,,,,,,,,, 29 | cue list,109,Test 28,,,,,green,,,,,,,,,,,, 30 | cart,113,Test 29,,,,,blue,,,,,,,,,,,, 31 | cue cart,117,Test 30,,,,,purple,,,,,,,,,,,, 32 | cue cart,121,Test 31,,,,,red,,,,,,,,,,,, 33 | midi,MT 1,Midi Test 1,Midi Test 1,,,,orange,2,1,1,12,,,,,50,50,, 34 | midi,MT 2,Midi Test 2,Midi Test 2,,,,green,2,16,2,10,10,10,,,25,20,, 35 | network,NW 1,Network Test 1,,,,,purple,,,,,,,,,,,1,/go 36 | network,NW 2,Network Test 2,,,,,purple,,,,,,,,,,,1,/go -------------------------------------------------------------------------------- /app/static/example_file/csv_test_doc_qlab_4.csv: -------------------------------------------------------------------------------- 1 | Type,Number,Name,Notes,continueMode,Target,File Target,Color,MIDI Message Type,Midi Command Format,Midi Command,Midi Device ID,Midi Control Number,Midi Control Value,Midi Patch Name,Midi Patch Number,Midi Q List,Midi Q Number,message type,command,osc cue number 2 | audio,1,Test 1,,1,,,red,,,,,,,,,,,,, 3 | mic,5,Test 2,,2,,,orange,,,,,,,,,,,,, 4 | video,9,Test 3,,3,,,green,,,,,,,,,,,,, 5 | camera,13,Test 4,,1,,,blue,,,,,,,,,,,,, 6 | text,17,Test 5,,2,,,purple,,,,,,,,,,,,, 7 | light,21,Test 6,,3,,,red,,,,,,,,,,,,, 8 | fade,25,Test 7,,2,,,orange,,,,,,,,,,,,, 9 | network,29,Test 8,,3,,,green,,,,,,,,,,,,, 10 | midi,33,Test 9,,1,,,blue,,,,,,,,,,,,, 11 | midi file,37,Test 10,,2,,,purple,,,,,,,,,,,,, 12 | timecode,41,Test 11,,3,,,red,,,,,,,,,,,,, 13 | group,45,Test 12,,2,,,orange,,,,,,,,,,,,, 14 | start,49,Test 13,,3,5,,green,,,,,,,,,,,,, 15 | stop,53,Test 14,,1,5,,blue,,,,,,,,,,,,, 16 | pause,57,Test 15,,2,5,,purple,,,,,,,,,,,,, 17 | load,61,Test 16,,3,5,,red,,,,,,,,,,,,, 18 | reset,65,Test 17,,2,5,,orange,,,,,,,,,,,,, 19 | devamp,69,Test 18,,3,9,,green,,,,,,,,,,,,, 20 | goto,73,Test 19,,1,5,,blue,,,,,,,,,,,,, 21 | target,77,Test 20,,2,57,,purple,,,,,,,,,,,,, 22 | arm,81,Test 21,,3,5,,red,,,,,,,,,,,,, 23 | disarm,85,Test 22,,2,5,,orange,,,,,,,,,,,,, 24 | wait,89,Test 23,,3,,,green,,,,,,,,,,,,, 25 | memo,93,Test 24,,0,,,blue,,,,,,,,,,,,, 26 | script,97,Test 25,,0,,,purple,,,,,,,,,,,,, 27 | list,101,Test 26,,,,,red,,,,,,,,,,,,, 28 | cuelist,105,Test 27,,,,,orange,,,,,,,,,,,,, 29 | cue list,109,Test 28,,,,,green,,,,,,,,,,,,, 30 | cart,113,Test 29,,,,,blue,,,,,,,,,,,,, 31 | cue cart,117,Test 30,,,,,purple,,,,,,,,,,,,, 32 | cue cart,121,Test 31,,,,,red,,,,,,,,,,,,, 33 | midi,MT 1,Midi Test 1,Midi Test 1,,,,orange,2,1,1,12,,,,,50,50,,, 34 | midi,MT 2,Midi Test 2,Midi Test 2,,,,green,2,16,2,10,10,10,,,25,20,,, 35 | network,NW 1,Network Test 1,,,,,purple,,,,,,,,,,,1,1,120 36 | network,NW 2,Network Test 2,,,,,purple,,,,,,,,,,,2,/go, -------------------------------------------------------------------------------- /app/application.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import webview 4 | 5 | from flask import Flask, render_template, request, redirect, url_for 6 | from werkzeug.utils import secure_filename 7 | 8 | from .csv_parser import send_csv 9 | from .helper import resource_path 10 | from .error_success_handler import return_errors, return_success, clear_errors_and_success 11 | 12 | if getattr(sys, "frozen", False): 13 | template_folder = resource_path("templates") 14 | static_folder = resource_path("static") 15 | app = Flask(__name__, template_folder=template_folder, static_folder=static_folder) 16 | else: 17 | app = Flask(__name__) 18 | 19 | app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 20 | app.config["UPLOAD_EXTENSIONS"] = [".csv"] 21 | app.config["UPLOAD_PATH"] = resource_path("static/csv_files") 22 | 23 | 24 | @app.route("/") 25 | def index(): 26 | clear_errors_and_success() 27 | return render_template("index.html") 28 | 29 | 30 | @app.route("/", methods=["POST"]) 31 | def upload_file(): 32 | uploaded_file = request.files["file"] 33 | ip = request.form["ip"] 34 | qlab_version = request.form["qlab-version"] 35 | passcode = request.form["passcode"] 36 | 37 | filename = secure_filename(uploaded_file.filename) 38 | if uploaded_file.filename != "": 39 | file_ext = os.path.splitext(filename)[1] 40 | if file_ext not in app.config["UPLOAD_EXTENSIONS"]: 41 | return "Invalid File", 400 42 | 43 | send_csv(ip, uploaded_file, int(qlab_version), passcode) 44 | if return_errors(): 45 | return render_template( 46 | "errors.html", errors=return_errors(), success=return_success() 47 | ) 48 | 49 | return redirect(url_for("success")) 50 | 51 | 52 | @app.route("/success") 53 | def success(): 54 | return render_template("success.html", successfull_commands=return_success()) 55 | 56 | 57 | @app.errorhandler(413) 58 | def too_large(e): 59 | return "File is too large", 413 60 | -------------------------------------------------------------------------------- /app/osc_server.py: -------------------------------------------------------------------------------- 1 | from pythonosc.osc_server import AsyncIOOSCUDPServer 2 | from pythonosc.dispatcher import Dispatcher 3 | import asyncio 4 | import json 5 | from .error_success_handler import handle_errors, count_success, ErrorHandler 6 | 7 | 8 | def async_osc_server(ip, port, error_handler=None): 9 | """ 10 | Start async OSC server to receive replies from QLab 11 | 12 | Args: 13 | ip: IP address to listen on 14 | port: Port to listen on 15 | error_handler: Optional ErrorHandler instance. If None, uses global handler. 16 | """ 17 | # Use provided handler or fall back to global functions 18 | if error_handler: 19 | def filter_handler(address, *args): 20 | data = json.loads(args[0]) 21 | if not data["status"] == "ok": 22 | error_handler.handle_errors(data["status"], f"{address}: {args}") 23 | else: 24 | error_handler.count_success(data["status"], f"{address}: {args}") 25 | else: 26 | def filter_handler(address, *args): 27 | data = json.loads(args[0]) 28 | if not data["status"] == "ok": 29 | handle_errors(data["status"], f"{address}: {args}") 30 | else: 31 | count_success(data["status"], f"{address}: {args}") 32 | 33 | dispatcher = Dispatcher() 34 | dispatcher.map("/reply/*", filter_handler) 35 | 36 | async def loop(): 37 | for i in range(1): 38 | await asyncio.sleep(0.05) 39 | 40 | async def init_main(): 41 | server = AsyncIOOSCUDPServer((ip, port), dispatcher, asyncio.get_event_loop()) 42 | ( 43 | transport, 44 | protocol, 45 | ) = ( 46 | await server.create_serve_endpoint() 47 | ) # Create datagram endpoint and start serving 48 | 49 | await loop() # Enter main loop of program 50 | 51 | transport.close() # Clean up serve endpoint 52 | 53 | asyncio.run(init_main()) 54 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'Easy to Use', 8 | Svg: require('../../static/img/pink-cake.svg').default, 9 | description: ( 10 | <> 11 | CSV to QLab was designed from the ground up to be easily installed and 12 | used to get your cues imported and running your shows quickly. 13 | 14 | ), 15 | }, 16 | { 17 | title: 'Saves Time', 18 | Svg: require('../../static/img/alarm-clock.svg').default, 19 | description: ( 20 | <> 21 | Often certain cues in QLab require essentially data entry. 22 | We already create cue sheets, why not use that to create the cues in QLab too? 23 | 24 | ), 25 | }, 26 | { 27 | title: 'Still Growing and Expanding', 28 | Svg: require('../../static/img/cartoon-rocket.svg').default, 29 | description: ( 30 | <> 31 | This is a modern project built to last. We will continue to update 32 | as suggestions come in. We want to improve your workflow so you can get back to 33 | running shows as quickly as possible. 34 | 35 | ), 36 | }, 37 | ]; 38 | 39 | function Feature({Svg, title, description}) { 40 | return ( 41 |
42 |
43 | 44 |
45 |
46 |

{title}

47 |

{description}

48 |
49 |
50 | ); 51 | } 52 | 53 | export default function HomepageFeatures() { 54 | return ( 55 |
56 |
57 |
58 | {FeatureList.map((props, idx) => ( 59 | 60 | ))} 61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/error_success_handler.py: -------------------------------------------------------------------------------- 1 | class ErrorHandler: 2 | """Handles errors and success messages for CSV to QLab operations""" 3 | 4 | def __init__(self): 5 | self.errors = [] 6 | self.success = [] 7 | 8 | def handle_errors(self, status, message): 9 | """Record an error message""" 10 | print("There was an error:") 11 | print(message) 12 | self.errors.append({"status": status, "message": message}) 13 | 14 | def count_success(self, status, message): 15 | """Record a success message""" 16 | self.success.append({"status": status, "message": message}) 17 | 18 | def get_errors(self): 19 | """Return all error messages""" 20 | return self.errors 21 | 22 | def get_success(self): 23 | """Return all success messages""" 24 | return self.success 25 | 26 | def has_errors(self): 27 | """Check if there are any errors""" 28 | return len(self.errors) > 0 29 | 30 | def clear(self): 31 | """Clear all errors and success messages""" 32 | self.errors.clear() 33 | self.success.clear() 34 | 35 | 36 | # Global instance for backward compatibility 37 | _global_handler = ErrorHandler() 38 | 39 | # Legacy global lists (for backward compatibility) 40 | errors = _global_handler.errors 41 | success = _global_handler.success 42 | 43 | 44 | def handle_errors(status, message): 45 | """Legacy function for backward compatibility""" 46 | _global_handler.handle_errors(status, message) 47 | 48 | 49 | def return_errors(): 50 | """Legacy function for backward compatibility""" 51 | return _global_handler.get_errors() 52 | 53 | 54 | def count_success(status, message): 55 | """Legacy function for backward compatibility""" 56 | _global_handler.count_success(status, message) 57 | 58 | 59 | def return_success(): 60 | """Legacy function for backward compatibility""" 61 | return _global_handler.get_success() 62 | 63 | 64 | def clear_errors_and_success(): 65 | """Legacy function for backward compatibility""" 66 | _global_handler.clear() 67 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: "Documentation" 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | paths: 11 | - 'website/**' 12 | pull_request: 13 | branches: [ main ] 14 | paths: 15 | - 'website/**' 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | jobs: 21 | spellcheck: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: rojopolis/spellcheck-github-actions@0.9.1 26 | name: Spellcheck 27 | 28 | checks: 29 | if: github.event_name != 'push' 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v1 33 | - uses: actions/setup-node@v1 34 | with: 35 | node-version: '>=14' 36 | - name: Test Build 37 | run: | 38 | cd website 39 | if [ -e yarn.lock ]; then 40 | yarn install --frozen-lockfile 41 | elif [ -e package-lock.json ]; then 42 | npm ci 43 | else 44 | npm i 45 | fi 46 | npm run build 47 | 48 | gh-release: 49 | if: github.event_name != 'pull_request' 50 | needs: [spellcheck] 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v1 54 | - uses: actions/setup-node@v1 55 | with: 56 | node-version: '>=14' 57 | - uses: webfactory/ssh-agent@v0.5.0 58 | with: 59 | ssh-private-key: ${{ secrets.GH_PAGES_DEPLOY }} 60 | - name: Release to GitHub Pages 61 | env: 62 | USE_SSH: true 63 | GIT_USER: git 64 | run: | 65 | cd website 66 | git config --global user.email "rossf.seg@gmail.com" 67 | git config --global user.name "fross123" 68 | if [ -e yarn.lock ]; then 69 | yarn install --frozen-lockfile 70 | elif [ -e package-lock.json ]; then 71 | npm ci 72 | else 73 | npm i 74 | fi 75 | npm run deploy -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from pathlib import Path 3 | 4 | # Read the README file 5 | readme_file = Path(__file__).parent / "README.md" 6 | long_description = readme_file.read_text() if readme_file.exists() else "" 7 | 8 | # Core dependencies (required for CLI) 9 | install_requires = [ 10 | 'python-osc>=1.8.3', 11 | ] 12 | 13 | # GUI-specific dependencies (optional) 14 | gui_requires = [ 15 | 'Flask>=3.0.3', 16 | 'pywebview>=5.1', 17 | ] 18 | 19 | # Development and testing dependencies 20 | dev_requires = [ 21 | 'pytest>=8.0.0', 22 | 'pytest-cov>=4.1.0', 23 | ] 24 | 25 | setup( 26 | name='csv-to-qlab', 27 | version='2025.1', 28 | description='Send CSV files to QLab via OSC', 29 | long_description=long_description, 30 | long_description_content_type='text/markdown', 31 | author='Finlay Ross', 32 | url='https://github.com/fross123/csv_to_qlab', 33 | packages=['app'], 34 | package_data={ 35 | 'app': [ 36 | 'qlab_osc_config.json', 37 | 'static/**/*', 38 | 'templates/**/*', 39 | ], 40 | }, 41 | include_package_data=True, 42 | install_requires=install_requires, 43 | extras_require={ 44 | 'gui': gui_requires, 45 | 'dev': dev_requires, 46 | 'all': gui_requires + dev_requires, 47 | }, 48 | entry_points={ 49 | 'console_scripts': [ 50 | 'csv-to-qlab=app.cli:main', 51 | ], 52 | }, 53 | python_requires='>=3.8', 54 | classifiers=[ 55 | 'Development Status :: 4 - Beta', 56 | 'Intended Audience :: Developers', 57 | 'Intended Audience :: End Users/Desktop', 58 | 'Topic :: Multimedia :: Sound/Audio', 59 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 60 | 'Programming Language :: Python :: 3', 61 | 'Programming Language :: Python :: 3.8', 62 | 'Programming Language :: Python :: 3.9', 63 | 'Programming Language :: Python :: 3.10', 64 | 'Programming Language :: Python :: 3.11', 65 | 'Programming Language :: Python :: 3.12', 66 | 'Programming Language :: Python :: 3.13', 67 | ], 68 | keywords='qlab osc csv automation theatre theater sound lighting', 69 | ) 70 | -------------------------------------------------------------------------------- /website/src/components/StatusBadges.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | // import styles from './HomepageFeatures.module.css'; 5 | 6 | const BadgeList = [ 7 | { 8 | link: 'https://img.shields.io/github/v/release/fross123/csv_to_qlab?style=for-the-badge', 9 | alt: "version Number" 10 | }, 11 | // { 12 | // link: 'https://img.shields.io/badge/Donate-PayPal?style=for-the-badge&logo=paypal&labelColor=lightgrey&color=blue&link=https%3A%2F%2Fwww.paypal.com%2Fpaypalme%2Ffinlayross', 13 | // alt: 'donate-paypal', 14 | // button_link: "https://www.paypal.com/paypalme/FinlayRoss" 15 | // }, 16 | { 17 | link: 'https://img.shields.io/github/downloads/fross123/csv_to_qlab/total?style=for-the-badge&label=All%20Downloads', 18 | alt: 'total-downloads' 19 | }, 20 | { 21 | link: 'https://img.shields.io/github/downloads/fross123/csv_to_qlab/latest/total?style=for-the-badge', 22 | alt: 'latest-version-downloads' 23 | }, 24 | { 25 | link: "https://img.shields.io/github/release-date/fross123/csv_to_qlab?style=for-the-badge&label=Last%20Release", 26 | alt: 'last-updated' 27 | }, 28 | { 29 | link: 'https://img.shields.io/github/license/fross123/csv_to_qlab?style=for-the-badge', 30 | alt: 'license' 31 | }, 32 | { 33 | link: 'https://img.shields.io/github/actions/workflow/status/fross123/csv_to_qlab/pytest.yml?style=for-the-badge&label=Pytest', 34 | alt: 'pytest-status' 35 | }, 36 | 37 | ] 38 | 39 | const OperatingSystemsList = [ 40 | { 41 | link: 'https://img.shields.io/badge/Works_On-MacOS_11_or_later-blue?style=for-the-badge&logo=apple', 42 | alt: 'works on logo', 43 | }, 44 | ] 45 | 46 | 47 | function Badge({link, alt, button_link}) { 48 | if (button_link) { 49 | return ( 50 | 52 | {alt} 53 | 54 | ) 55 | } else { 56 | return ( 57 | {alt} 58 | ) 59 | } 60 | } 61 | 62 | export default function Badges() { 63 | return ( 64 |
65 | {BadgeList.map((props, idx) => ( 66 | 67 | ))} 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /app/templates/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | Errors 23 | 24 | 25 | 26 |
27 | 28 | 34 |
35 |
36 |
37 | 39 | 41 | {% for error in errors %} 42 |

43 | Error Status: {{ error.status }}
44 | {{ error.message }} 45 |

46 | {% endfor %} 47 | Back 48 |
49 |
50 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/tests/test_app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from app.application import app 3 | 4 | 5 | def test_home_page(): 6 | """ 7 | Test response from homepage. 8 | """ 9 | response = app.test_client().get("/") 10 | assert response.status_code == 200 11 | 12 | 13 | def test_simple_csv(): 14 | """ 15 | Tests simple.csv submission. When qlab open, tests return. 16 | """ 17 | resources = Path(__file__).parent.parent / "static" / "example_file" 18 | 19 | response = app.test_client().post( 20 | "/", 21 | data={ 22 | "file": (resources / "simple.csv").open("rb"), 23 | "ip": "127.0.0.1", 24 | "passcode": "3420", 25 | "qlab-version": "5", 26 | }, 27 | follow_redirects=True, 28 | ) 29 | 30 | assert response.status_code == 200 31 | assert len(response.history) == 1 32 | assert response.request.path == "/success" 33 | 34 | 35 | def test_example_csv(): 36 | """ 37 | Tests csv_test_doc.csv. When QLab is open, also tests return. 38 | """ 39 | resources = Path(__file__).parent.parent / "static" / "example_file" 40 | 41 | response = app.test_client().post( 42 | "/", 43 | data={ 44 | "file": (resources / "csv_test_doc.csv").open("rb"), 45 | "ip": "127.0.0.1", 46 | "passcode": "3420", 47 | "qlab-version": "5", 48 | }, 49 | follow_redirects=True, 50 | ) 51 | 52 | assert response.status_code == 200 53 | assert len(response.history) == 1 54 | assert response.request.path == "/success" 55 | 56 | 57 | def test_ql4_csv(): 58 | """ 59 | Test .csv doc and test for previous versions of qlab. 60 | """ 61 | resources = Path(__file__).parent.parent / "static" / "example_file" 62 | 63 | response = app.test_client().post( 64 | "/", 65 | data={ 66 | "file": (resources / "csv_test_doc_qlab_4.csv").open("rb"), 67 | "ip": "127.0.0.1", 68 | "passcode": "3420", 69 | "qlab-version": "4", 70 | }, 71 | follow_redirects=True, 72 | ) 73 | 74 | assert response.status_code == 200 75 | assert len(response.history) == 1 76 | assert response.request.path == "/success" 77 | 78 | 79 | def test_no_filename(): 80 | """ 81 | Ensure that files with no name are invalid. 82 | """ 83 | response = app.test_client().post( 84 | "/", 85 | data={ 86 | "file": "", 87 | "ip": "127.0.0.1", 88 | }, 89 | ) 90 | 91 | assert response.status_code == 400 92 | -------------------------------------------------------------------------------- /app/templates/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | 32 | 33 | Success 34 | 35 | 36 | 37 |
38 | 39 | 45 |
46 |
47 |
48 |

Congratulations! Your request was successful

49 | 51 | Back 52 | Funny success image 55 |
56 |
57 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/static/js/index.js: -------------------------------------------------------------------------------- 1 | // JavaScript for disabling form submissions if there are invalid fields 2 | (function () { 3 | 'use strict'; 4 | window.addEventListener('load', function () { 5 | // Fetch all the forms we want to apply custom Bootstrap validation styles to 6 | var forms = document.getElementsByClassName('needs-validation'); 7 | // Loop over them and prevent submission 8 | var validation = Array.prototype.filter.call(forms, function (form) { 9 | form.addEventListener('submit', function (event) { 10 | var validFileExtensions = [".csv"]; 11 | var fileElement = document.getElementById('csv_file'); 12 | if (form.checkValidity() === false) { 13 | event.preventDefault(); 14 | event.stopPropagation(); 15 | } 16 | form.classList.add('was-validated'); 17 | }, false); 18 | }); 19 | 20 | document.getElementById("csv_file").onchange = function () { 21 | 22 | ValidateSingleInput(document.getElementById("csv_file")) 23 | } 24 | }, false); 25 | })(); 26 | 27 | var _validFileExtensions = [".csv"]; 28 | function ValidateSingleInput(oInput) { 29 | if (oInput.type == "file") { 30 | var sFileName = oInput.value; 31 | if (sFileName.length > 0) { 32 | var blnValid = false; 33 | for (var j = 0; j < _validFileExtensions.length; j++) { 34 | var sCurExtension = _validFileExtensions[j]; 35 | if (sFileName.substr(sFileName.length - sCurExtension.length, sCurExtension.length).toLowerCase() == sCurExtension.toLowerCase()) { 36 | blnValid = true; 37 | break; 38 | } 39 | } 40 | 41 | if (blnValid) { 42 | oInput.classList.remove('is-invalid') 43 | oInput.classList.add('is-valid'); 44 | return false; 45 | } else { 46 | oInput.classList.remove('is-valid'); 47 | oInput.classList.add('is-invalid'); 48 | return false; 49 | } 50 | } 51 | } 52 | return true; 53 | } 54 | 55 | document.addEventListener('DOMContentLoaded', (e) => { 56 | var ql5v = document.querySelector('#passcode-switch'); 57 | var ql5group = document.querySelector('#passcode-group') 58 | ql5v.addEventListener('click', (e) => { 59 | if (ql5group.hidden) { // if the form is hidden 60 | ql5group.hidden = false; 61 | } else { // hide group 62 | ql5group.hidden = true; 63 | } 64 | }) 65 | }) -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import styles from './index.module.css'; 7 | import HomepageFeatures from '../components/HomepageFeatures'; 8 | import Badges from '../components/StatusBadges.js'; 9 | import Logo from '@site/static/img/logo.svg'; 10 | 11 | import DropdownButton from './../components/DropdownButton.js'; 12 | 13 | 14 | function HomepageHeader() { 15 | const {siteConfig} = useDocusaurusContext(); 16 | const options = [ 17 | { label: 'macOS 15 ARM (Latest)', value: 'macos15-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS15-ARM.dmg' }, 18 | { label: 'macOS 14 ARM', value: 'macos14-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS14-ARM.dmg' }, 19 | { label: 'macOS 11+ Intel', value: 'macos-intel', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab.dmg' }, 20 | ]; 21 | const buttonClassName = "button button--secondary button--lg" 22 | 23 | 24 | return ( 25 |
26 |
27 |
28 | New with v2025.1
CSV to QLab is now available as a CLI as well as a macOS application! 29 |
Download below, or read the installation instructions. 30 |
31 | 32 |

{siteConfig.title}

33 |

{siteConfig.tagline}

34 |
35 | 38 | CSV Import Tutorial 39 | 40 |
41 | 42 |
43 |

Or... skip ahead and download now!

44 | 45 | 46 | 47 | 48 |
49 |
50 | ); 51 | } 52 | 53 | export default function Home() { 54 | const {siteConfig} = useDocusaurusContext(); 55 | return ( 56 | 59 | 60 |
61 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | name: Draft Release 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Build and Draft Release 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | # Intel build via self-hosted runner (macOS 11.7, supports macOS 11+) 19 | - os: [self-hosted, macOS, X64] 20 | TARGET: macos 21 | CMD_BUILD: > 22 | pyinstaller application.spec && 23 | cd dist/ && 24 | zip -r9 csv-to-qlab.zip csv-to-qlab.app 25 | OUT_FILE_NAME: csv-to-qlab.app 26 | ZIP_FILE_NAME: csv-to-qlab.zip 27 | BASE_FILE_NAME: CSV-To-QLab 28 | # ARM builds via GitHub-hosted runners 29 | - os: macos-14 30 | TARGET: macos 31 | CMD_BUILD: > 32 | pyinstaller application.spec && 33 | cd dist/ && 34 | mv csv-to-qlab.app csv-to-qlab-macos14-arm.app && 35 | zip -r9 csv-to-qlab-macos14-arm.zip csv-to-qlab-macos14-arm.app 36 | OUT_FILE_NAME: csv-to-qlab-macos14-arm.app 37 | ZIP_FILE_NAME: csv-to-qlab-macos14-arm.zip 38 | BASE_FILE_NAME: CSV-To-QLab-macOS14-ARM 39 | - os: macos-15 40 | TARGET: macos 41 | CMD_BUILD: > 42 | pyinstaller application.spec && 43 | cd dist/ && 44 | mv csv-to-qlab.app csv-to-qlab-macos15-arm.app && 45 | zip -r9 csv-to-qlab-macos15-arm.zip csv-to-qlab-macos15-arm.app 46 | OUT_FILE_NAME: csv-to-qlab-macos15-arm.app 47 | ZIP_FILE_NAME: csv-to-qlab-macos15-arm.zip 48 | BASE_FILE_NAME: CSV-To-QLab-macOS15-ARM 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up Python 3.10 53 | if: ${{ !contains(matrix.os, 'self-hosted') }} 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: "3.10" 57 | - name: Install dependencies 58 | run: | 59 | python3 -m pip install --upgrade pip 60 | pip3 install -r requirements.txt 61 | pip3 install pyinstaller 62 | - name: Build with pyinstaller for ${{matrix.TARGET}} 63 | run: ${{matrix.CMD_BUILD}} 64 | - name: Create DMG 65 | run: | 66 | brew install create-dmg || true 67 | create-dmg \ 68 | --volname "${{ matrix.BASE_FILE_NAME }}" \ 69 | --window-pos 300 200 \ 70 | --window-size 450 300 \ 71 | --icon-size 100 \ 72 | --app-drop-link 330 150 \ 73 | --icon ${{ matrix.OUT_FILE_NAME }} 100 150 \ 74 | --skip-jenkins \ 75 | --eula COPYING \ 76 | ${{ matrix.BASE_FILE_NAME }}.dmg \ 77 | ./dist/${{ matrix.OUT_FILE_NAME }} 78 | - name: Draft Release 79 | id: draft-release 80 | uses: softprops/action-gh-release@v1 81 | with: 82 | draft: true 83 | files: | 84 | ./${{ matrix.BASE_FILE_NAME }}.dmg 85 | ./dist/${{ matrix.ZIP_FILE_NAME }} -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Pytest 5 | 6 | on: 7 | 8 | push: 9 | branches: [ "main" ] 10 | paths-ignore: 11 | - 'website/**' 12 | - '.github/workflows/documentation.yml' 13 | pull_request: 14 | branches: [ "main" ] 15 | paths-ignore: 16 | - 'website/**' 17 | - '.github/workflows/documentation.yml' 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | pytest: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up Python 3.10 29 | uses: actions/setup-python@v3 30 | with: 31 | python-version: "3.10" 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install flake8 pytest 36 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 37 | - name: Lint with flake8 38 | run: | 39 | # stop the build if there are Python syntax errors or undefined names 40 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 41 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 42 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 43 | - name: Test with pytest 44 | run: | 45 | pytest 46 | 47 | # release: 48 | # name: Build and Draft Release 49 | # needs: [pytest] 50 | # if: startsWith(github.ref, 'refs/tags/') 51 | # runs-on: ${{ matrix.os }} 52 | # strategy: 53 | # matrix: 54 | # include: 55 | # - os: macos-11 56 | # TARGET: macos 57 | # CMD_BUILD: > 58 | # pyinstaller application.spec && 59 | # cd dist/ && 60 | # zip -r9 csv-to-qlab.zip csv-to-qlab.app 61 | # OUT_FILE_NAME: csv-to-qlab.app 62 | # ZIP_FILE_NAME: csv-to-qlab.zip 63 | # steps: 64 | # - uses: actions/checkout@v4 65 | # - name: Set up Python 3.8 66 | # uses: actions/setup-python@v5 67 | # with: 68 | # python-version: 3.8 69 | # - name: Install dependencies 70 | # run: | 71 | # python -m pip install --upgrade pip 72 | # pip install -r requirements.txt 73 | # pip install pyinstaller 74 | # - name: Build with pyinstaller for ${{matrix.TARGET}} 75 | # run: ${{matrix.CMD_BUILD}} 76 | # - name: Create DMG 77 | # run: | 78 | # brew install create-dmg 79 | # create-dmg \ 80 | # --volname "CSV-To-QLab" \ 81 | # --window-pos 300 200 \ 82 | # --window-size 450 300 \ 83 | # --icon-size 100 \ 84 | # --app-drop-link 330 150 \ 85 | # --icon ${{ matrix.OUT_FILE_NAME }} 100 150 \ 86 | # CSV-To-QLab.dmg \ 87 | # ./dist/${{ matrix.OUT_FILE_NAME }} 88 | # - name: Draft Release 89 | # id: draft-release 90 | # uses: softprops/action-gh-release@v1 91 | # with: 92 | # draft: true 93 | # files: | 94 | # ./CSV-To-QLab.dmg 95 | # ./dist/${{ matrix.ZIP_FILE_NAME }} -------------------------------------------------------------------------------- /app/static/example_file/csv_test_doc.csv: -------------------------------------------------------------------------------- 1 | Type,Number,Name,Notes,Continue Mode,follow,Target,File Target,Color,MIDI Message Type,Midi Command Format,Midi Command,Midi Device ID,Midi Control Number,Midi Control Value,Midi Patch Name,Midi Patch Number,Midi Q List,Midi Q Number,Network Patch Number,Custom String,Stop Target When Done,Stage Number,Fade Opacity,Midi Raw String 2 | audio,audio-1,Test 1,,1,,,,red,,,,,,,,,,,,,,,, 3 | mic,mic-5,Test 2,,,2,,,orange,,,,,,,,,,,,,,,, 4 | video,vid-9,Test 3,,3,,,app/static/example_file/broken_heart.png,green,,,,,,,,,,,,,,1,, 5 | camera,cam-13,Test 4,,1,,,,blue,,,,,,,,,,,,,,,, 6 | text,txt-17,Test 5,,2,,,,purple,,,,,,,,,,,,,,,, 7 | light,lx-21,Test 6,,3,,,,red,,,,,,,,,,,,,,,, 8 | fade,fade-1,stop target when done is checked.,,2,,,,orange,,,,,,,,,,,,,true,,, 9 | fade,fade-2,test-fade-false-stop-target-when-done,stop target when done should not be selected,,,,,,,,,,,,,,,,,,false,,, 10 | network,nw-29,Test 8,,3,,,,green,,,,,,,,,,,,,,,, 11 | midi,midi-33,Test 9,,1,,,,blue,,,,,,,,,,,,,,,, 12 | midi file,midi-f-37,Test 10,,2,,,,purple,,,,,,,,,,,,,,,, 13 | timecode,tc-41,Test 11,,3,,,,red,,,,,,,,,,,,,,,, 14 | group,grp-45,Test 12,,2,,,,orange,,,,,,,,,,,,,,,, 15 | start,start-49,Test 13,,3,,5,,green,,,,,,,,,,,,,,,, 16 | stop,stop-53,Test 14,,1,,5,,blue,,,,,,,,,,,,,,,, 17 | pause,pause-57,Test 15,,2,,5,,purple,,,,,,,,,,,,,,,, 18 | load,load-61,Test 16,,3,,5,,red,,,,,,,,,,,,,,,, 19 | reset,reset-65,Test 17,,2,,5,,orange,,,,,,,,,,,,,,,, 20 | devamp,devamp-69,Test 18,,3,,9,,green,,,,,,,,,,,,,,,, 21 | goto,goto-73,Test 19,,1,,5,,blue,,,,,,,,,,,,,,,, 22 | target,target-77,Test 20,,2,,57,,purple,,,,,,,,,,,,,,,, 23 | arm,arm-81,Test 21,,3,,5,,red,,,,,,,,,,,,,,,, 24 | disarm,disarm-85,Test 22,,2,,5,,orange,,,,,,,,,,,,,,,, 25 | wait,wait-89,Test 23,,3,,,,green,,,,,,,,,,,,,,,, 26 | memo,memo-93,Test 24,,0,,,,blue,,,,,,,,,,,,,,,, 27 | script,script-97,Test 25,,0,,,,purple,,,,,,,,,,,,,,,, 28 | list,list-101,Test 26,,,,,,red,,,,,,,,,,,,,,,, 29 | cuelist,cuelist-105,Test 27,,,,,,orange,,,,,,,,,,,,,,,, 30 | cue list,cue_list-109,Test 28,,,,,,green,,,,,,,,,,,,,,,, 31 | cart,cart-113,Test 29,,,,,,blue,,,,,,,,,,,,,,,, 32 | cue cart,cue-cart-117,Test 30,,,,,,purple,,,,,,,,,,,,,,,, 33 | cue cart,cue-cart-121,Test 31,,,,,,red,,,,,,,,,,,,,,,, 34 | midi,midi-MT 1,Midi Test 1,Midi Test 1,,,,,orange,2,1,1,12,,,,,50,50,,,,,, 35 | midi,MT 2,Midi Test 2,Midi Test 2,,,,,green,2,16,2,10,10,10,,,25,20,,,,,, 36 | network,NW 1,Network Test 1,,,,,,purple,,,,,,,,,,,1,/go,,,, 37 | network,NW 2,Network Test 2,,,,,,purple,,,,,,,,,,,1,/go,,,, 38 | memo,color-1,color-1,,,,,,berry,,,,,,,,,,,,,,,, 39 | memo,color-2,color-2,,,,,,blue,,,,,,,,,,,,,,,, 40 | memo,color-3,color-3,,,,,,crimson,,,,,,,,,,,,,,,, 41 | memo,color-4,color-4,,,,,,cyan,,,,,,,,,,,,,,,, 42 | memo,color-5,color-5,,,,,,forest,,,,,,,,,,,,,,,, 43 | memo,color-6,color-6,,,,,,gray,,,,,,,,,,,,,,,, 44 | memo,color-7,color-7,,,,,,green,,,,,,,,,,,,,,,, 45 | memo,color-8,color-8,,,,,,hot pink,,,,,,,,,,,,,,,, 46 | memo,color-9,color-9,,,,,,indigo,,,,,,,,,,,,,,,, 47 | memo,color-10,color-10,,,,,,lavender,,,,,,,,,,,,,,,, 48 | memo,color-11,color-11,,,,,,magenta,,,,,,,,,,,,,,,, 49 | memo,color-12,color-12,,,,,,midnight,,,,,,,,,,,,,,,, 50 | memo,color-13,color-13,,,,,,olive,,,,,,,,,,,,,,,, 51 | memo,color-14,color-14,,,,,,orange,,,,,,,,,,,,,,,, 52 | memo,color-15,color-15,,,,,,peach,,,,,,,,,,,,,,,, 53 | memo,color-16,color-16,,,,,,plum,,,,,,,,,,,,,,,, 54 | memo,color-17,color-17,,,,,,purple,,,,,,,,,,,,,,,, 55 | memo,color-18,color-18,,,,,,red,,,,,,,,,,,,,,,, 56 | memo,color-19,color-19,,,,,,sky blue,,,,,,,,,,,,,,,, 57 | memo,color-20,color-20,,,,,,yellow,,,,,,,,,,,,,,,, 58 | video,vid-test-fade,Video Test Fade,,,,,,,,,,,,,,,,,,,,1,, 59 | fade,video-fade-out,Video Fade Opacity Zero,,,,vid-test-fade,,,,,,,,,,,,,,,,,0, 60 | midi,midi-sysex-test-1,Midi SysEx Test 1,,,,,,,3,,,,,,,,,,,,,,,testing-raw-string -------------------------------------------------------------------------------- /app/csv_parser.py: -------------------------------------------------------------------------------- 1 | import io 2 | import csv 3 | 4 | from pythonosc import osc_message_builder, osc_bundle_builder, udp_client, osc_server 5 | from .osc_server import async_osc_server 6 | from .osc_config import get_osc_config 7 | 8 | 9 | def send_csv(ip, document, qlab_version, passcode, error_handler=None): 10 | """ 11 | Sends the data in csv file to qlab workspace on machine with given ip. 12 | Uses dynamic configuration from qlab_osc_config.json 13 | 14 | Args: 15 | ip: IP address of QLab machine 16 | document: File-like object containing CSV data 17 | qlab_version: QLab version (4 or 5) 18 | passcode: Optional passcode for QLab connection 19 | error_handler: Optional ErrorHandler instance. If None, uses global handler. 20 | """ 21 | # Get OSC configuration 22 | osc_config = get_osc_config() 23 | 24 | client = udp_client.UDPClient(ip, 53000) 25 | 26 | stream = io.StringIO(document.stream.read().decode("UTF8"), newline="") 27 | reader = csv.reader(stream) 28 | 29 | # Retrieve row one from csv document and use as headers. 30 | headers = [x.lower().replace(" ", "") for x in next(reader)] 31 | 32 | cues = [] 33 | 34 | # Build cue list to be sent to qlab. 35 | for line in reader: 36 | count = 0 37 | cue = {} 38 | for header in headers: 39 | cue[header] = line[count] 40 | count += 1 41 | cues.append(cue) 42 | 43 | # Connect with passcode if provided 44 | if passcode: 45 | msg = osc_message_builder.OscMessageBuilder(address="/connect") 46 | msg.add_arg(passcode) 47 | client.send(msg.build()) 48 | 49 | # Process each cue 50 | for cue in cues: 51 | bundle = osc_bundle_builder.OscBundleBuilder(osc_bundle_builder.IMMEDIATELY) 52 | 53 | # Create new cue 54 | cue_type = cue.get("type", "memo").lower() 55 | validated_cue_type = osc_config.check_cue_type(cue_type) 56 | 57 | if validated_cue_type: 58 | msg = osc_message_builder.OscMessageBuilder(address="/new") 59 | msg.add_arg(validated_cue_type) 60 | bundle.add_content(msg.build()) 61 | else: 62 | # Cue type is invalid, create memo cue 63 | msg = osc_message_builder.OscMessageBuilder(address="/new") 64 | msg.add_arg("memo") 65 | bundle.add_content(msg.build()) 66 | validated_cue_type = "memo" 67 | 68 | # Track properties that have been set to avoid duplicates 69 | processed_properties = set() 70 | 71 | # Process all properties dynamically using configuration 72 | for header, value in cue.items(): 73 | # Skip if no value or already processed 74 | if not value or header in processed_properties or header == 'type': 75 | continue 76 | 77 | # Build OSC message using configuration 78 | osc_msg = osc_config.build_osc_message( 79 | property_name=header, 80 | value=value, 81 | cue_type=validated_cue_type, 82 | qlab_version=qlab_version, 83 | cue_data=cue 84 | ) 85 | 86 | if osc_msg: 87 | bundle.add_content(osc_msg.build()) 88 | processed_properties.add(header) 89 | 90 | # Check for auto-properties (e.g., doopacity when fadeopacity is set) 91 | auto_props = osc_config.get_auto_properties(header, validated_cue_type) 92 | for auto_prop_name, auto_prop_value in auto_props: 93 | auto_msg = osc_config.build_osc_message( 94 | property_name=auto_prop_name, 95 | value=auto_prop_value, 96 | cue_type=validated_cue_type, 97 | qlab_version=qlab_version 98 | ) 99 | if auto_msg: 100 | bundle.add_content(auto_msg.build()) 101 | 102 | # Send the bundle 103 | client.send(bundle.build()) 104 | async_osc_server(ip, 53001, error_handler) 105 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | CSV to QLab 23 | 24 | 25 | 26 |
27 | 28 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | 44 |
45 |
46 | 47 | 48 |
49 | 57 |
58 | 59 |
60 |
61 | 123.456.7.89 62 |
63 | 65 |
66 | Please enter a vaild ip address. 67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 |
75 | Only .csv files accepted. 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/static/example_file/example_all.csv: -------------------------------------------------------------------------------- 1 | Number,Target,Page,Type,continueMode,Name,Notes,Color,midiCommand,midiCommandFormat,midiControlNumber,midiDeviceID,midiMessageType,midiPatchName,midiPatchNumber,midiqList,midiqNumber,midiqPath,midiRawString,midiStatus,Text,,,,,,,,,,,,, 2 | 1,,,start,1,house and works,FYI this will only “Target” currently created cues.,green,,,,,,,,,,,,,,,,,,,,,,,,,, 3 | 5,1,,START,2,lamp warmup,FYI this will only “Target” currently created cues.,green,,,,,,,,,,,,,,,,,,,,,,,,,, 4 | LX 5.1,,,midi,2,house and works,,red,1,1,123,3,2,“none”,,main,12,,,,,,,,,,,,,,,,, 5 | SQ 10,,,midi,1,Preshow,FYI this will only “Target” currently created cues.,green,1,1,123,3,2,,0,main,13,,,,,,,,,,,,,,,,, 6 | PQ 15,,,network,1,House Half,,purple,,,,,,,,,,,,,,,,,,,,,,,,,, 7 | PQ 20,,,network,2,House out,“16 counts”,purple,,,,,,,,,,,,,,,,,,,,,,,,,, 8 | PQ 20,,,network,2,Stage Up,complete with finish in music,purple,,,,,,,,,,,,,,,,,,,,,,,,,, 9 | PQ 30,,13,network,1,more up in well,very very slow fade up,purple,,,,,,,,,,,,,,,,,,,,,,,,,, 10 | 35,,19,start,1,add more upstairs,very very slow fade up (1:30),green,,,,,,,,,,,,,,,,,,,,,,,,,, 11 | 40,,24,start,,well out,very slow fade down,green,,,,,,,,,,,,,,,,,,,,,,,,,, 12 | 45,,30ish,fade,,well up,very very slow fade up,orange,,,,,,,,,,,,,,,,,,,,,,,,,, 13 | 50,,34,start,,well out,very slow fade down,green,,,,,,,,,,,,,,,,,,,,,,,,,, 14 | 55,,36,midi,,well up,very very slow fade up,red,,,,,,,,,,,,,,,,,,,,,,,,,, 15 | 60,,41,midi,,well out,very slow fade down,red,,,,,,,,,,,,,,,,,,,,,,,,,, 16 | 65,,61,midi,,pull down upstairs,very slow fade down,red,,,,,,,,,,,,,,,,,,,,,,,,,, 17 | 70,,67,midi,,blackout,two or three parts? each in quick succession,red,,,,,,,,,,,,,,,,,,,,,,,,,, 18 | 75,,67,midi,,INTERMISSION,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 19 | 80,,69,midi,,House Half,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 20 | 85,,69,midi,,House out,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 21 | 90,,69,midi,,Stage down,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 22 | 95,,69,midi,,Up on Tim,"fairly quick up; very dark “streetlamp corner”; focus on Tim SL, back lit",red,,,,,,,,,,,,,,,,,,,,,,,,,, 23 | 96,,,midi,,Stage Up for scene light,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 24 | 100,,76,midi,,add light on stairs/second level,very very slow fade up; build in parts,red,,,,,,,,,,,,,,,,,,,,,,,,,, 25 | 105,,84,midi,,lights up “onstage”,"quick, big and noticeable",red,,,,,,,,,,,,,,,,,,,,,,,,,, 26 | 110,,108,midi,,lights change onstage,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 27 | 115,,108,midi,,wrong lighting cue,something colorful,red,,,,,,,,,,,,,,,,,,,,,,,,,, 28 | 116,,,midi,,restore back to correct light cue,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 29 | 120,,114,midi,,lights flicker,"quick, fairly subtle",red,,,,,,,,,,,,,,,,,,,,,,,,,, 30 | 125,,115,midi,,lights flicker twice,"once at landing, once at bottom",red,,,,,,,,,,,,,,,,,,,,,,,,,, 31 | 130,,131,midi,,backstage lights snap off,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 32 | 135,,131,midi,,“Stage” lights snap off,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 33 | 140,,131,midi,,INTERMISSION,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 34 | 145,,133,midi,,House Half,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 35 | 150,,133,midi,,House out,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 36 | 155,,133,midi,,Stage Out,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 37 | 156,,133,midi,,False start / stage up,slow fade up,red,,,,,,,,,,,,,,,,,,,,,,,,,, 38 | 157,,133,midi,,blackout,fast,red,,,,,,,,,,,,,,,,,,,,,,,,,, 39 | 160,,133,midi,,Stage Up; Tim in “spotlight”,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 40 | 165,,133/134,midi,,“follow spot” follows Tim,intentionally jittery,red,,,,,,,,,,,,,,,,,,,,,,,,,, 41 | 166,,134,midi,,“,“,red,,,,,,,,,,,,,,,,,,,,,,,,,, 42 | 167,,134,midi,,“,“,red,,,,,,,,,,,,,,,,,,,,,,,,,, 43 | 168,,134,midi,,“,“,red,,,,,,,,,,,,,,,,,,,,,,,,,, 44 | 169,,134,midi,,“,“,red,,,,,,,,,,,,,,,,,,,,,,,,,, 45 | 170,,134,midi,,spot out,spot out with a jerk? / leave preshow,red,,,,,,,,,,,,,,,,,,,,,,,,,, 46 | 175,,134,midi,,First look of “Nothing On”,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 47 | 180,,139,midi,,add more upstairs,very very slow fade up,red,,,,,,,,,,,,,,,,,,,,,,,,,, 48 | 185,,147,midi,,lights flicker twice (?),will tweak based on sound fx,red,,,,,,,,,,,,,,,,,,,,,,,,,, 49 | 190,,155,midi,,lights flicker (biggest),work top/bax into it somehow,red,,,,,,,,,,,,,,,,,,,,,,,,,, 50 | 193,,159,midi,,smoke from fireplace,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 51 | 195,,161,midi,,quick blackout,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 52 | 200,,,midi,,stage up for curtain call,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 53 | 205,,,midi,,bow lights?,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 54 | 210,,,midi,,stage down,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 55 | 215,,,midi,,House Up,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 56 | 220,,,midi,,house and works,,red,,,,,,,,,,,,,,,,,,,,,,,,,, 57 | 300,,,text,,TEsting text cues,,purple,,,,,,,,,,,,,Testing-1-2-3-4-1-2-3,,,,,,,,,,,,, -------------------------------------------------------------------------------- /website/releases/2025-10-3-v2025.1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 2025/1 3 | title: Version 2025.1 4 | tags: ["2025", "1"] 5 | --- 6 | 7 | import Link from '@docusaurus/Link'; 8 | import useBaseUrl from '@docusaurus/useBaseUrl'; 9 | import styles from '@site/src/pages/index.module.css'; 10 | import DropdownButton from '@site/src/components/DropdownButton.js'; 11 | 12 | export const options = [ 13 | { label: 'macOS 15 ARM (Latest)', value: 'macos15-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2025.1/CSV-To-QLab-macOS15-ARM.dmg' }, 14 | { label: 'macOS 14 ARM', value: 'macos14-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2025.1/CSV-To-QLab-macOS14-ARM.dmg' }, 15 | { label: 'macOS 11+ Intel', value: 'macos-intel', link: 'https://github.com/fross123/csv_to_qlab/releases/download/v2025.1/CSV-To-QLab.dmg' }, 16 | ]; 17 | export const buttonClassName = "button button--primary button--lg" 18 | 19 | ## Major Architecture Refactor 20 | 21 | This release features a significant refactor to a **configuration-driven architecture**, making it easier to maintain and extend CSV to QLab. 22 | 23 | :::info Build Changes 24 | Starting with this release, builds are provided for: 25 | - **macOS 11+ Intel** - Supports all Intel Macs running macOS 11 or later 26 | - **macOS 14 ARM** - For Apple Silicon Macs 27 | - **macOS 15 ARM** - For Apple Silicon Macs 28 | 29 | Older release versions remain available in [release history](https://github.com/fross123/csv_to_qlab/releases). 30 | ::: 31 | 32 | ### New Features 33 | 34 | - **Command-Line Interface (CLI)** - New cross-platform CLI for automation and scripting workflows 35 | - Install with `pip install .` for CLI-only or `pip install .[gui]` for full GUI support 36 | - Run with `csv-to-qlab show.csv 127.0.0.1 5` 37 | - Supports verbose, quiet, and JSON output modes 38 | - Perfect for batch processing, automation, and remote/SSH sessions 39 | - See [CLI Advanced](/docs/tutorial-basics/cli-advanced) for automation and scripting 40 | - **JSON-Based Configuration** - All OSC properties now defined in `qlab_osc_config.json` 41 | - **Enhanced Property Support** - Added many new global and cue-specific properties 42 | - **Auto-Properties** - Automatic enabling of related settings (e.g., setting fade opacity auto-enables the checkbox) 43 | - **Improved Validation** - Better type checking and value validation for all properties 44 | 45 | ### New Cue Properties 46 | 47 | - **Global Properties**: Armed, Flagged, Auto Load, Duration, Pre/Post Wait 48 | - **Audio/Video Cues**: Level, Rate, Pitch, Loop controls, Start/End Time, Patch, Gang 49 | - **Fade Cues**: Fade And Stop Others, Fade And Stop Others Time, Do Volume, Do Fade 50 | - **All existing MIDI and Network properties** retained and improved 51 | 52 | ### Developer Improvements 53 | 54 | - **No-Code Property Addition** - Add new OSC properties by editing JSON config only 55 | - **Comprehensive Documentation** - New developer and reference documentation 56 | - **Better Error Handling** - Improved validation and error reporting with instance-based error handlers 57 | - **Comprehensive Test Suite** - 69 tests with 86% code coverage (added 19 CLI tests) 58 | - **Duplicate Property Detection** - Automated tests prevent configuration conflicts 59 | - **Package Setup** - New `setup.py` for pip installation with separate GUI and CLI dependencies 60 | - **Proper Package Structure** - Follows Python/PyInstaller best practices with `app/` as a proper Python package 61 | 62 | ### Documentation 63 | 64 | - New [CLI Documentation](/docs/tutorial-basics/cli-advanced) with automation and scripting examples 65 | - New [Developer Documentation](/docs/developer/architecture) section 66 | - New [CSV Column Reference](/docs/reference/csv-columns) with all available properties 67 | - New [Testing Guide](/docs/developer/testing) for contributors 68 | - Updated tutorials with expanded examples and unified flow 69 | 70 | 71 | 72 | --- 73 | 74 | ### Bug Fixes (v2025.1.1 - Hotfix) 75 | 76 | - **Fixed CLI Installation** - Resolved `ModuleNotFoundError` when installing CLI via pip 77 | - Added `app/__init__.py` to properly mark `app/` as a Python package 78 | - Converted all intra-package imports to relative imports (following PEP 8) 79 | - Created `run_gui.py` as external PyInstaller entry point (following PyInstaller best practices) 80 | - Updated all test imports to use proper package structure 81 | - All 69 tests now pass 82 | 83 | --- 84 | 85 | For contributors: See the [Architecture Overview](/docs/developer/architecture) and [Adding Properties Guide](/docs/developer/adding-properties) for details on the new system. 86 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer').themes.github; 5 | const darkCodeTheme = require('prism-react-renderer').themes.dracula; 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'CSV to QLab', 10 | tagline: 'Import csv files into a QLab workspace easily and efficiently.', 11 | url: 'https://csv-to-qlab.finlayross.me', 12 | baseUrl: '/', 13 | onBrokenLinks: 'throw', 14 | onBrokenMarkdownLinks: 'warn', 15 | favicon: 'img/favicon.ico', 16 | organizationName: 'fross123', // Usually your GitHub org/user name. 17 | projectName: 'csv_to_qlab', // Usually your repo name. 18 | trailingSlash: true, 19 | 20 | presets: [ 21 | [ 22 | '@docusaurus/preset-classic', 23 | /** @type {import('@docusaurus/preset-classic').Options} */ 24 | ({ 25 | gtag: { 26 | trackingID: 'G-VTFHGK2SN2', 27 | // Optional fields. 28 | anonymizeIP: true, // Should IPs be anonymized? 29 | }, 30 | docs: { 31 | sidebarPath: require.resolve('./sidebars.js'), 32 | // Please change this to your repo. 33 | editUrl: 'https://github.com/fross123/csv_to_qlab/edit/main/website/', 34 | }, 35 | blog: { 36 | showReadingTime: false, 37 | // Please change this to your repo. 38 | editUrl: 39 | 'https://github.com/fross123/csv_to_qlab/edit/main/website/releases/', 40 | blogTitle: 'CSV to QLab Releases', 41 | blogDescription: 'CSV to QLab Release Log', 42 | blogSidebarTitle: "Recent Releases", 43 | postsPerPage: 'ALL', 44 | routeBasePath: "releases", 45 | path: './releases', 46 | }, 47 | theme: { 48 | customCss: require.resolve('./src/css/custom.css'), 49 | }, 50 | sitemap: { 51 | changefreq: 'weekly', 52 | priority: 0.5, 53 | }, 54 | }), 55 | ], 56 | ], 57 | themeConfig: 58 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 59 | ({ 60 | colorMode: { 61 | defaultMode: 'dark', 62 | respectPrefersColorScheme: true, 63 | }, 64 | image: 'img/logo.png', 65 | // announcementBar: { 66 | // id: 'support_us', 67 | // content: 68 | // '🌟 If you are enjoying CSV to QLab please give us a star on Github!!', 69 | // backgroundColor: 'rgba(0, 0, 0, 0.3)', 70 | // textColor: '#091E42', 71 | // isCloseable: false, 72 | // }, 73 | navbar: { 74 | title: 'CSV to QLab', 75 | logo: { 76 | alt: 'csv-to-qlab-Logo', 77 | src: 'img/csv_to_qlab_logo.svg', 78 | }, 79 | items: [ 80 | { 81 | type: 'doc', 82 | docId: 'intro', 83 | position: 'left', 84 | label: 'Tutorial', 85 | }, 86 | {to: '/releases', label: 'Releases', position: 'left'}, 87 | { 88 | href: 'https://buymeacoffee.com/finlayross', 89 | label: 'Buy Me A Coffee', 90 | position: 'right', 91 | className: 'button button--primary button--large' 92 | 93 | }, 94 | { 95 | href: 'https://github.com/fross123/csv_to_qlab', 96 | label: 'GitHub', 97 | position: 'right', 98 | }, 99 | ], 100 | }, 101 | footer: { 102 | style: 'dark', 103 | links: [ 104 | { 105 | title: 'Docs', 106 | items: [ 107 | { 108 | label: 'Tutorial', 109 | to: '/docs/intro', 110 | }, 111 | ], 112 | }, 113 | { 114 | title: 'More', 115 | items: [ 116 | { 117 | label: 'Releases', 118 | to: '/releases', 119 | }, 120 | { 121 | label: 'GitHub', 122 | href: 'https://github.com/fross123/csv_to_qlab', 123 | }, 124 | ], 125 | }, 126 | ], 127 | copyright: `Copyright © ${new Date().getFullYear()} CSV to QLab, Inc. Built with Docusaurus.`, 128 | }, 129 | prism: { 130 | theme: lightCodeTheme, 131 | darkTheme: darkCodeTheme, 132 | }, 133 | }), 134 | scripts: [ 135 | // String format. 136 | '/js/brevo.js' 137 | ], 138 | }; 139 | 140 | module.exports = config; 141 | -------------------------------------------------------------------------------- /website/docs/tutorial-basics/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import Link from '@docusaurus/Link'; 6 | import styles from '@site/src/pages/index.module.css'; 7 | import useBaseUrl from '@docusaurus/useBaseUrl'; 8 | 9 | import DropdownButton from '@site/src/components/DropdownButton.js'; 10 | 11 | 12 | # Installation 13 | 14 | CSV to QLab is available as both a GUI application (Mac only) and a cross-platform command-line interface. 15 | 16 | :::tip 👉 Choose Your Path 17 | - **Mac GUI users**: Continue with the GUI Application section below 18 | - **CLI users**: Jump to [CLI Installation](#command-line-interface-cli) 19 | ::: 20 | 21 | ## GUI Application (Recommended for Mac Users) 22 | 23 | Download CSV to QLab from the [latest GitHub release](https://github.com/fross123/csv_to_qlab/releases/). Check out the [latest release notes](/releases) for the latest updates. 24 | 25 | export const options = [ 26 | { label: 'macOS 15 ARM (Latest)', value: 'macos15-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS15-ARM.dmg' }, 27 | { label: 'macOS 14 ARM', value: 'macos14-arm', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS14-ARM.dmg' }, 28 | { label: 'macOS 11+ Intel', value: 'macos-intel', link: 'https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab.dmg' }, 29 | ]; 30 | export const buttonClassName = "button button--primary button--lg" 31 | 32 | 33 | 34 | :::note 35 | You will eventually need QLab by Figure53. Download at [qlab.app](https://qlab.app/) 36 | ::: 37 | 38 | 39 | ## Agree to the terms of use 40 | CSV to QLab operates with a [GNU General Public License v3.0](https://github.com/fross123/csv_to_qlab/COPYING). 41 | 42 | ![license screenshot](/img/tutorial/license-screenshot.png) 43 | 44 | 45 | ## Move Application 46 | 47 | As requested after you agree to the terms of use, move the application to the Applications folder on your computer. 48 | 49 | ![Move the App](/img/tutorial/move-app-screenshot.png) 50 | 51 | ## Eject disk image 52 | 53 | Open finder, and eject the disk image. You will not need this anymore. 54 | 55 | You will see the item below on the left sidebar of the finder under a heading "locations". Click the eject button to eject the installer. 56 | 57 | ![Eject Disk Image](/img/tutorial/eject-screenshot.png) 58 | 59 | 60 | ## Open Application 61 | 62 | Go to your Application folder. Often found in the Finder sidebar. Open CSV to QLab. 63 | 64 | 65 | ## Confirm Security Settings 66 | 67 | Apple only verifies applications under their "Developer" program, which has a yearly fee of $99. This is a side project, and therefore does not have a developer key attached. The code is public, you may confirm that the software is not malicious by going to the [GitHub](https://www.github.com/fross123/csv_to_qlab/) repository. 68 | 69 | More info on [Apple Certificates](https://developer.apple.com/support/certificates/). 70 | 71 | :::tip 72 | You will only need to do these steps one time per-download. 73 | ::: 74 | 75 | ### 1. Confirm the initial message received 76 | 77 | ![test](/img/tutorial/security-message-1.png) 78 | 79 | ### 2. Click "open anyway" in security and privacy 80 | - Open System Preferences 81 | - Click Security and Privacy 82 | - Click the button that says "Open Anyway" 83 | 84 | :::caution 85 | This instruction is not to be taken as advice to ignore Apple policies and/or standards. This software is free and open source, use at your own risk. 86 | ::: 87 | 88 | ![Open Anyway](/img/tutorial/open-anyway-screenshot.png) 89 | 90 | ### 3. Click "Open" one last time 91 | 92 | ![Security Message 3](/img/tutorial/security-message-3.png) 93 | 94 | 95 | ## CSV to QLab should open 96 | 97 | ![Application Screenshot](/img/tutorial/app-screenshot-ud.png) 98 | 99 | --- 100 | 101 | ## Command-Line Interface (CLI) 102 | 103 | For automation, scripting, or cross-platform use, install the CLI with pip: 104 | 105 | ```bash 106 | # Clone the repository 107 | git clone https://github.com/fross123/csv_to_qlab.git 108 | cd csv_to_qlab 109 | 110 | # Install 111 | pip install . 112 | ``` 113 | 114 | **Next Steps:** 115 | - Continue to [Prepare a CSV File](/docs/tutorial-basics/prepare-csv-file) to format your data 116 | - Learn basic usage in [Send to QLab](/docs/tutorial-basics/send-to-qlab#using-the-cli) 117 | - Explore [CLI Advanced](/docs/tutorial-basics/cli-advanced) for automation and scripting 118 | 119 | :::tip When to Use CLI vs GUI 120 | - **Use CLI**: Automation, scripting, batch processing, remote/SSH sessions, Linux/Windows 121 | - **Use GUI**: Quick one-off imports, visual feedback, Mac users 122 | ::: -------------------------------------------------------------------------------- /website/docs/tutorial-basics/send-to-qlab.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Send to QLab 6 | 7 | Follow these steps to send your prepared CSV file to QLab. Instructions are provided for both the GUI application and command-line interface. 8 | 9 | ## Prerequisites 10 | 11 | Before sending your CSV file, make sure you have: 12 | 13 | 1. **QLab is running** - Open QLab with a workspace ready 14 | 2. **CSV file is prepared** - See [Prepare a CSV File](/docs/tutorial-basics/prepare-csv-file) 15 | 3. **Know your QLab version** - QLab 4 or 5 16 | 4. **Know the IP address** - See below 17 | 18 | :::note QLab Version Support 19 | CSV to QLab works with both QLab 4 and QLab 5, though some features are only available on QLab 5. 20 | ::: 21 | 22 | ### Finding the IP Address 23 | 24 | You'll need the IP address of the machine running QLab: 25 | 26 | - **Same computer**: Use `127.0.0.1` (localhost) 27 | - **Different computer on network**: 28 | - **Mac**: System Preferences → Network → Look for "IP Address" 29 | - **Windows**: Open Command Prompt → Type `ipconfig` → Look for "IPv4 Address" 30 | - **Linux**: Terminal → Type `ip addr` or `ifconfig` 31 | 32 | ### OSC Passcode (Optional) 33 | 34 | If your QLab workspace has an OSC passcode: 35 | - Find it in QLab: Settings/Preferences → OSC → Passcode 36 | - Make sure the passcode has full access to the workspace 37 | - On QLab 5, you can also use the "no passcode" option 38 | 39 | --- 40 | 41 | ## Using the GUI Application 42 | 43 | ### 1. Open CSV to QLab 44 | 45 | Launch the CSV to QLab application on your Mac. 46 | 47 | ### 2. Select QLab Version 48 | 49 | Choose QLab 4 or 5 from the dropdown. QLab 5 handles some incoming messages slightly differently. 50 | 51 | ### 3. Enter Passcode (If Required) 52 | 53 | If your QLab workspace uses an OSC passcode: 54 | - Check the passcode checkbox 55 | - Enter the passcode from QLab settings 56 | 57 | :::tip 58 | It's also possible to bypass this step in QLab 5 by allowing access with the "no passcode" option. 59 | ::: 60 | 61 | ### 4. Enter IP Address 62 | 63 | Enter the IP address of the machine running QLab: 64 | - `127.0.0.1` if running locally 65 | - Network IP like `192.168.1.100` if on a different machine 66 | 67 | ### 5. Select Your CSV File 68 | 69 | Click "Choose File" and select your prepared CSV file. 70 | 71 | ### 6. Submit 72 | 73 | Click the submit button and keep the QLab workspace open. 74 | 75 | ### 7. Success! 76 | 77 | You should see a success page and your cues will appear in QLab! 78 | 79 | ![Success Page](/img/funny-success-quote-1-picture-quote-1.jpg) 80 | 81 | --- 82 | 83 | ## Using the CLI 84 | 85 | ### Basic Usage 86 | 87 | Open your terminal and run: 88 | 89 | ```bash 90 | csv-to-qlab your-file.csv 127.0.0.1 5 91 | ``` 92 | 93 | Replace with your values: 94 | - `your-file.csv` - Path to your CSV file 95 | - `127.0.0.1` - IP address of QLab machine 96 | - `5` - QLab version (4 or 5) 97 | 98 | ### With Passcode 99 | 100 | If your QLab workspace requires a passcode: 101 | 102 | ```bash 103 | csv-to-qlab your-file.csv 192.168.1.100 5 --passcode 1234 104 | ``` 105 | 106 | ### Verbose Output 107 | 108 | See detailed progress information: 109 | 110 | ```bash 111 | csv-to-qlab your-file.csv 127.0.0.1 5 --verbose 112 | ``` 113 | 114 | Example output: 115 | ``` 116 | Sending CSV file: your-file.csv 117 | QLab IP: 127.0.0.1 118 | QLab version: 5 119 | 120 | ✓ Successfully processed 55 cue(s) 121 | ``` 122 | 123 | ### JSON Output (For Automation) 124 | 125 | Perfect for scripts and automation: 126 | 127 | ```bash 128 | csv-to-qlab your-file.csv 127.0.0.1 5 --json 129 | ``` 130 | 131 | ### More CLI Options 132 | 133 | For advanced usage including batch processing, automation, and scripting, see the [CLI Advanced](/docs/tutorial-basics/cli-advanced) documentation. 134 | 135 | --- 136 | 137 | ## Troubleshooting 138 | 139 | ### No Cues Appearing in QLab 140 | 141 | - Verify QLab is running and a workspace is open 142 | - Check that the IP address is correct 143 | - Ensure your firewall isn't blocking connections 144 | - Try using `127.0.0.1` if running on the same machine 145 | 146 | ### Passcode Errors 147 | 148 | - Double-check the passcode in QLab settings (Settings → OSC → Passcode) 149 | - Ensure the passcode has full workspace access 150 | - Try the "no passcode" option in QLab 5 if available 151 | 152 | ### CSV Format Errors 153 | 154 | - Verify your CSV has the required columns: **Number**, **Type**, **Name** 155 | - Check the [CSV Column Reference](/docs/reference/csv-columns) for proper formatting 156 | - Try with a [simple example file](https://github.com/fross123/csv_to_qlab/blob/main/app/static/example_file/simple.csv) first 157 | 158 | ### Network Issues 159 | 160 | - Ensure both machines are on the same network 161 | - Test connectivity: `ping [IP_ADDRESS]` 162 | - Check firewall settings on both machines 163 | - Verify QLab's OSC settings allow incoming connections 164 | 165 | ### Connection Refused 166 | 167 | - Make sure QLab is running before sending the CSV 168 | - Check that OSC is enabled in QLab preferences 169 | - Verify the port is 53000 (default for QLab) 170 | 171 | --- 172 | 173 | ## Next Steps 174 | 175 | - Learn about [all available CSV columns](/docs/reference/csv-columns) 176 | - Explore [CLI automation and batch processing](/docs/tutorial-basics/cli-advanced) 177 | - Check out the [example CSV files](https://github.com/fross123/csv_to_qlab/tree/main/app/static/example_file) 178 | 179 | :::tip 180 | If you encounter an error not listed here, please submit an [issue on GitHub](https://github.com/fross123/csv_to_qlab/issues/new/choose). We're here to help! 181 | ::: 182 | -------------------------------------------------------------------------------- /website/docs/tutorial-basics/cli-advanced.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # CLI Advanced Usage 6 | 7 | Advanced features for automation, scripting, and batch processing with the CSV to QLab command-line interface. 8 | 9 | :::info Prerequisites 10 | This guide assumes you've already: 11 | - [Installed the CLI](/docs/tutorial-basics/installation#command-line-interface-cli) 12 | - Learned [basic CLI usage](/docs/tutorial-basics/send-to-qlab#using-the-cli) 13 | - Prepared a [CSV file](/docs/tutorial-basics/prepare-csv-file) 14 | ::: 15 | 16 | ## Output Modes 17 | 18 | ### Human-Readable Output (Default) 19 | Shows a summary of successes and errors: 20 | 21 | ```bash 22 | csv-to-qlab show.csv 127.0.0.1 5 23 | ``` 24 | 25 | Output: 26 | ``` 27 | ✓ Successfully processed 55 cue(s) 28 | ``` 29 | 30 | ### Verbose Mode 31 | Shows detailed information during processing: 32 | 33 | ```bash 34 | csv-to-qlab show.csv 127.0.0.1 5 --verbose 35 | ``` 36 | 37 | Output: 38 | ``` 39 | Sending CSV file: show.csv 40 | QLab IP: 127.0.0.1 41 | QLab version: 5 42 | Using passcode: **** 43 | 44 | ✓ Successfully processed 55 cue(s) 45 | ``` 46 | 47 | ### Quiet Mode 48 | Suppresses success messages, only shows errors: 49 | 50 | ```bash 51 | csv-to-qlab show.csv 127.0.0.1 5 --quiet 52 | ``` 53 | 54 | ### JSON Output 55 | Perfect for scripting and automation: 56 | 57 | ```bash 58 | csv-to-qlab show.csv 127.0.0.1 5 --json 59 | ``` 60 | 61 | Output: 62 | ```json 63 | { 64 | "success": [ 65 | { 66 | "status": "ok", 67 | "message": "/reply/new: ..." 68 | } 69 | ], 70 | "errors": [], 71 | "has_errors": false 72 | } 73 | ``` 74 | 75 | ## Command-Line Arguments 76 | 77 | ### Positional Arguments (Required) 78 | - `csv_file` - Path to CSV file containing cue data 79 | - `ip` - IP address of QLab machine (e.g., 127.0.0.1 or 192.168.1.100) 80 | - `qlab_version` - QLab version: either 4 or 5 81 | 82 | ### Optional Arguments 83 | - `-p, --passcode PASSCODE` - QLab workspace passcode 84 | - `-v, --verbose` - Enable verbose output 85 | - `-q, --quiet` - Suppress success messages, only show errors 86 | - `-j, --json` - Output results in JSON format 87 | - `-h, --help` - Show help message and exit 88 | 89 | ## Automation Examples 90 | 91 | ### Batch Processing Multiple Files 92 | Process multiple CSV files in sequence: 93 | 94 | ```bash 95 | #!/bin/bash 96 | for file in cues/*.csv; do 97 | csv-to-qlab "$file" 127.0.0.1 5 --quiet 98 | if [ $? -ne 0 ]; then 99 | echo "Error processing $file" 100 | fi 101 | done 102 | ``` 103 | 104 | ### Integration with Scripts 105 | Use JSON output for programmatic processing: 106 | 107 | ```bash 108 | #!/bin/bash 109 | result=$(csv-to-qlab show.csv 127.0.0.1 5 --json) 110 | has_errors=$(echo "$result" | jq -r '.has_errors') 111 | 112 | if [ "$has_errors" = "true" ]; then 113 | echo "Errors occurred during processing" 114 | echo "$result" | jq -r '.errors[]' 115 | exit 1 116 | fi 117 | 118 | echo "Success!" 119 | ``` 120 | 121 | ### Watch Directory for Changes 122 | Automatically process CSV files when they appear in a directory: 123 | 124 | ```bash 125 | #!/bin/bash 126 | # Requires inotifywait (Linux) or fswatch (macOS) 127 | 128 | # macOS 129 | fswatch -o ~/cue_drops | while read; do 130 | for file in ~/cue_drops/*.csv; do 131 | csv-to-qlab "$file" 127.0.0.1 5 --quiet 132 | mv "$file" ~/cue_drops/processed/ 133 | done 134 | done 135 | ``` 136 | 137 | ### Remote Execution via SSH 138 | Run the CLI on a remote machine: 139 | 140 | ```bash 141 | ssh user@server "csv-to-qlab /path/to/show.csv 127.0.0.1 5" 142 | ``` 143 | 144 | ## Exit Codes 145 | 146 | The CLI uses standard exit codes for automation: 147 | 148 | - `0` - Success (no errors occurred) 149 | - `1` - Error (file not found, processing errors, etc.) 150 | 151 | Use in scripts: 152 | ```bash 153 | if csv-to-qlab show.csv 127.0.0.1 5 --quiet; then 154 | echo "Success!" 155 | else 156 | echo "Failed with exit code $?" 157 | fi 158 | ``` 159 | 160 | ## Troubleshooting 161 | 162 | ### Command Not Found 163 | If `csv-to-qlab` command is not found after installation: 164 | 165 | 1. Check if pip's bin directory is in your PATH: 166 | ```bash 167 | python -m pip show csv-to-qlab 168 | ``` 169 | 170 | 2. Try running directly: 171 | ```bash 172 | python -m app.cli show.csv 127.0.0.1 5 173 | ``` 174 | 175 | ### Module Not Found Errors 176 | Ensure python-osc is installed: 177 | 178 | ```bash 179 | pip install python-osc 180 | ``` 181 | 182 | ### Connection Errors 183 | - Verify QLab is running on the target machine 184 | - Check that OSC is enabled in QLab preferences 185 | - Ensure the IP address is correct 186 | - Verify network connectivity: `ping [IP_ADDRESS]` 187 | 188 | ### CSV Format Errors 189 | - Ensure your CSV has the required columns (Number, Type, Name) 190 | - Check that column headers match the [CSV Column Reference](/docs/reference/csv-columns) 191 | - Verify the CSV is properly formatted (no extra quotes, commas, etc.) 192 | 193 | ## Development Mode 194 | 195 | For development, install in editable mode: 196 | 197 | ```bash 198 | # Clone and navigate to repository 199 | git clone https://github.com/fross123/csv_to_qlab.git 200 | cd csv_to_qlab 201 | 202 | # Install in editable mode 203 | pip install -e . 204 | 205 | # Run directly from source 206 | python app/cli.py show.csv 127.0.0.1 5 207 | ``` 208 | 209 | This allows you to modify the code and test changes immediately without reinstalling. 210 | 211 | ## Next Steps 212 | 213 | - Explore the [CSV Column Reference](/docs/reference/csv-columns) for all available properties 214 | - Learn about [Developer Documentation](/docs/developer/architecture) if you want to contribute 215 | - Share your automation scripts with the community 216 | -------------------------------------------------------------------------------- /app/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Command-line interface for CSV to QLab 4 | 5 | This module provides a CLI for sending CSV files to QLab without the GUI. 6 | """ 7 | 8 | import sys 9 | import argparse 10 | import json 11 | import io 12 | from pathlib import Path 13 | 14 | from .csv_parser import send_csv 15 | from .error_success_handler import ErrorHandler 16 | 17 | 18 | class FileStorageAdapter: 19 | """Adapter to make a file object compatible with Flask's FileStorage interface""" 20 | 21 | def __init__(self, file_path): 22 | self.file_path = Path(file_path) 23 | with open(self.file_path, 'rb') as f: 24 | self.stream = io.BytesIO(f.read()) 25 | 26 | 27 | def format_human_readable(error_handler): 28 | """Format errors and successes in human-readable format""" 29 | output = [] 30 | 31 | errors = error_handler.get_errors() 32 | successes = error_handler.get_success() 33 | 34 | if successes: 35 | output.append(f"✓ Successfully processed {len(successes)} cue(s)") 36 | 37 | if errors: 38 | output.append(f"\n✗ Encountered {len(errors)} error(s):") 39 | for error in errors: 40 | output.append(f" - {error['status']}: {error['message']}") 41 | 42 | return "\n".join(output) if output else "No cues processed" 43 | 44 | 45 | def format_json(error_handler): 46 | """Format errors and successes as JSON""" 47 | return json.dumps({ 48 | "success": error_handler.get_success(), 49 | "errors": error_handler.get_errors(), 50 | "has_errors": error_handler.has_errors() 51 | }, indent=2) 52 | 53 | 54 | def main(): 55 | """Main CLI entry point""" 56 | parser = argparse.ArgumentParser( 57 | description="Send CSV files to QLab via OSC", 58 | formatter_class=argparse.RawDescriptionHelpFormatter, 59 | epilog=""" 60 | Examples: 61 | # Basic usage 62 | csv-to-qlab show.csv 127.0.0.1 5 63 | 64 | # With passcode 65 | csv-to-qlab show.csv 192.168.1.100 5 --passcode 1234 66 | 67 | # Quiet output (errors only) 68 | csv-to-qlab show.csv 127.0.0.1 5 --quiet 69 | 70 | # JSON output for scripting 71 | csv-to-qlab show.csv 127.0.0.1 5 --json 72 | """ 73 | ) 74 | 75 | parser.add_argument( 76 | 'csv_file', 77 | type=str, 78 | help='Path to CSV file containing cue data' 79 | ) 80 | 81 | parser.add_argument( 82 | 'ip', 83 | type=str, 84 | help='IP address of QLab machine (e.g., 127.0.0.1)' 85 | ) 86 | 87 | parser.add_argument( 88 | 'qlab_version', 89 | type=int, 90 | choices=[4, 5], 91 | help='QLab version (4 or 5)' 92 | ) 93 | 94 | parser.add_argument( 95 | '-p', '--passcode', 96 | type=str, 97 | default='', 98 | help='QLab workspace passcode (optional)' 99 | ) 100 | 101 | parser.add_argument( 102 | '-v', '--verbose', 103 | action='store_true', 104 | help='Enable verbose output' 105 | ) 106 | 107 | parser.add_argument( 108 | '-q', '--quiet', 109 | action='store_true', 110 | help='Suppress success messages, only show errors' 111 | ) 112 | 113 | parser.add_argument( 114 | '-j', '--json', 115 | action='store_true', 116 | help='Output results in JSON format' 117 | ) 118 | 119 | args = parser.parse_args() 120 | 121 | # Validate CSV file exists 122 | csv_path = Path(args.csv_file) 123 | if not csv_path.exists(): 124 | print(f"Error: CSV file not found: {args.csv_file}", file=sys.stderr) 125 | return 1 126 | 127 | if not csv_path.is_file(): 128 | print(f"Error: Path is not a file: {args.csv_file}", file=sys.stderr) 129 | return 1 130 | 131 | # Create error handler 132 | error_handler = ErrorHandler() 133 | 134 | # Suppress print statements in error handler for quiet/json mode 135 | if args.quiet or args.json: 136 | error_handler.handle_errors = lambda status, message: error_handler.errors.append({ 137 | "status": status, 138 | "message": message 139 | }) 140 | 141 | try: 142 | # Create file adapter 143 | document = FileStorageAdapter(args.csv_file) 144 | 145 | if args.verbose and not args.json: 146 | print(f"Sending CSV file: {args.csv_file}") 147 | print(f"QLab IP: {args.ip}") 148 | print(f"QLab version: {args.qlab_version}") 149 | if args.passcode: 150 | print(f"Using passcode: {'*' * len(args.passcode)}") 151 | print() 152 | 153 | # Send CSV to QLab 154 | send_csv( 155 | ip=args.ip, 156 | document=document, 157 | qlab_version=args.qlab_version, 158 | passcode=args.passcode, 159 | error_handler=error_handler 160 | ) 161 | 162 | # Output results 163 | if args.json: 164 | print(format_json(error_handler)) 165 | elif not args.quiet: 166 | output = format_human_readable(error_handler) 167 | if output: 168 | print(output) 169 | 170 | # Return exit code based on errors 171 | return 1 if error_handler.has_errors() else 0 172 | 173 | except FileNotFoundError as e: 174 | error_msg = f"Error: File not found: {e}" 175 | if args.json: 176 | print(json.dumps({"error": error_msg, "success": [], "errors": []})) 177 | else: 178 | print(error_msg, file=sys.stderr) 179 | return 1 180 | 181 | except Exception as e: 182 | error_msg = f"Error: {str(e)}" 183 | if args.json: 184 | print(json.dumps({"error": error_msg, "success": [], "errors": []})) 185 | else: 186 | print(error_msg, file=sys.stderr) 187 | if args.verbose: 188 | import traceback 189 | traceback.print_exc() 190 | return 1 191 | 192 | 193 | if __name__ == '__main__': 194 | sys.exit(main()) 195 | -------------------------------------------------------------------------------- /app/generate_column_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate CSV column documentation from qlab_osc_config.json 4 | 5 | This script reads the OSC configuration and outputs a markdown table 6 | showing all available CSV columns for each cue type. 7 | """ 8 | 9 | import json 10 | from helper import resource_path 11 | 12 | 13 | def format_header_name(config_key): 14 | """Convert config key back to human-readable header name""" 15 | # Handle special cases and known prefixes 16 | special_mappings = { 17 | 'midi': 'MIDI', 18 | 'osc': 'OSC', 19 | 'qlab': 'QLab', 20 | 'id': 'ID', 21 | 'prewait': 'Pre Wait', 22 | 'postwait': 'Post Wait', 23 | 'filetarget': 'File Target', 24 | 'groupmode': 'Group Mode', 25 | 'continuemode': 'Continue Mode', 26 | 'autoload': 'Auto Load', 27 | 'stoptargetwhendone': 'Stop Target When Done', 28 | 'fadeopacity': 'Fade Opacity', 29 | 'stagenumber': 'Stage Number', 30 | 'infiniteloop': 'Infinite Loop', 31 | 'playcount': 'Play Count', 32 | 'starttime': 'Start Time', 33 | 'endtime': 'End Time', 34 | 'dovolume': 'Do Volume', 35 | 'dofade': 'Do Fade', 36 | 'doopacity': 'Do Opacity', 37 | 'fadeandstopothers': 'Fade And Stop Others', 38 | 'fadeandstopotherstime': 'Fade And Stop Others Time', 39 | 'customstring': 'Custom String', 40 | 'networkpatchname': 'Network Patch Name', 41 | 'networkpatchnumber': 'Network Patch Number', 42 | 'osccuenumber': 'OSC Cue Number', 43 | 'rawstring': 'Raw String', 44 | # MIDI specific 45 | 'midicommand': 'MIDI Command', 46 | 'midicommandformat': 'MIDI Command Format', 47 | 'midicontrolnumber': 'MIDI Control Number', 48 | 'midicontrolvalue': 'MIDI Control Value', 49 | 'midideviceid': 'MIDI Device ID', 50 | 'midimessagetype': 'MIDI Message Type', 51 | 'midipatchname': 'MIDI Patch Name', 52 | 'midipatchnumber': 'MIDI Patch Number', 53 | 'midiqlist': 'MIDI Q List', 54 | 'midiqnumber': 'MIDI Q Number', 55 | 'midirawstring': 'MIDI Raw String', 56 | 'midistatus': 'MIDI Status' 57 | } 58 | 59 | # Check for exact match first 60 | if config_key in special_mappings: 61 | return special_mappings[config_key] 62 | 63 | # Handle MIDI prefixed properties 64 | if config_key.startswith('midi'): 65 | rest = config_key[4:] # Remove 'midi' prefix 66 | rest_formatted = format_header_name(rest) if rest else '' 67 | return f"MIDI {rest_formatted}".strip() 68 | 69 | # Default: capitalize each word 70 | return " ".join(word.capitalize() for word in config_key.split()) 71 | 72 | 73 | def load_config(): 74 | """Load the OSC configuration""" 75 | config_path = resource_path("qlab_osc_config.json") 76 | with open(config_path, 'r') as f: 77 | return json.load(f) 78 | 79 | 80 | def generate_docs(): 81 | """Generate documentation for all CSV columns""" 82 | config = load_config() 83 | 84 | print("# CSV Column Reference\n") 85 | print("This document lists all available CSV columns for creating QLab cues.\n") 86 | print("*This documentation is automatically generated from the OSC configuration.*\n") 87 | 88 | # Global Properties 89 | print("## Global Properties (All Cue Types)\n") 90 | print("These columns can be used with any cue type:\n") 91 | print("| CSV Column Header | Description | Type | Valid Values |") 92 | print("|------------------|-------------|------|--------------|") 93 | 94 | for key, prop in sorted(config['global_properties'].items()): 95 | header = format_header_name(key) 96 | description = prop.get('description', '') 97 | prop_type = prop.get('type', 'string') 98 | 99 | valid_values = "" 100 | if 'valid_range' in prop: 101 | valid_values = f"{prop['valid_range'][0]}-{prop['valid_range'][1]}" 102 | elif 'valid_values' in prop: 103 | # Show first few values 104 | values = prop['valid_values'][:5] 105 | valid_values = ", ".join(values) 106 | if len(prop['valid_values']) > 5: 107 | valid_values += "..." 108 | 109 | print(f"| {header} | {description} | {prop_type} | {valid_values} |") 110 | 111 | # Cue-Type Specific Properties 112 | print("\n## Cue-Type Specific Properties\n") 113 | 114 | for cue_type, properties in sorted(config['cue_type_properties'].items()): 115 | print(f"### {cue_type.capitalize()} Cues\n") 116 | 117 | # Handle nested structures (like network cues with qlab versions) 118 | if cue_type == 'network': 119 | for version, version_props in sorted(properties.items()): 120 | print(f"#### {version.upper()} Properties\n") 121 | print("| CSV Column Header | Description | Type |") 122 | print("|------------------|-------------|------|") 123 | 124 | for key, prop in sorted(version_props.items()): 125 | header = format_header_name(key) 126 | description = prop.get('description', '') 127 | prop_type = prop.get('type', 'string') 128 | print(f"| {header} | {description} | {prop_type} |") 129 | print() 130 | else: 131 | print("| CSV Column Header | Description | Type | Valid Values |") 132 | print("|------------------|-------------|------|--------------|") 133 | 134 | for key, prop in sorted(properties.items()): 135 | # Skip auto-properties (they're set automatically) 136 | if 'auto_value' in prop: 137 | continue 138 | 139 | header = format_header_name(key) 140 | description = prop.get('description', '') 141 | prop_type = prop.get('type', 'string') 142 | 143 | valid_values = "" 144 | if 'valid_range' in prop: 145 | valid_values = f"{prop['valid_range'][0]}-{prop['valid_range'][1]}" 146 | 147 | print(f"| {header} | {description} | {prop_type} | {valid_values} |") 148 | print() 149 | 150 | # Valid Cue Types 151 | print("## Valid Cue Types\n") 152 | print("Use these values in the `Type` column:\n") 153 | for cue_type in config['valid_cue_types']: 154 | print(f"- `{cue_type}`") 155 | 156 | 157 | if __name__ == "__main__": 158 | generate_docs() 159 | -------------------------------------------------------------------------------- /website/static/img/cartoon-rocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 36 | 37 | 38 | 39 | 41 | 43 | 45 | 47 | 48 | 50 | 51 | 52 | 55 | 56 | 59 | 60 | 61 | 63 | 64 | 65 | 68 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /website/static/img/pink-cake.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/osc_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pythonosc import osc_message_builder 3 | from .helper import resource_path 4 | 5 | 6 | class OSCConfig: 7 | """Load and manage OSC configuration from JSON file""" 8 | 9 | def __init__(self, config_path=None): 10 | if config_path is None: 11 | config_path = resource_path("qlab_osc_config.json") 12 | 13 | with open(config_path, 'r') as f: 14 | self.config = json.load(f) 15 | 16 | self.global_properties = self.config.get('global_properties', {}) 17 | self.cue_type_properties = self.config.get('cue_type_properties', {}) 18 | self.valid_cue_types = self.config.get('valid_cue_types', []) 19 | 20 | def get_valid_cue_types(self): 21 | """Return list of valid cue types""" 22 | return self.valid_cue_types 23 | 24 | def check_cue_type(self, cue_type): 25 | """Return the valid type of cue, or False""" 26 | cue_type = cue_type.lower() 27 | if cue_type not in self.valid_cue_types: 28 | return False 29 | return cue_type 30 | 31 | def get_property_config(self, property_name, cue_type=None, qlab_version=None): 32 | """ 33 | Get configuration for a specific property 34 | 35 | Args: 36 | property_name: The CSV column name (normalized to lowercase, no spaces) 37 | cue_type: Optional cue type for cue-specific properties 38 | qlab_version: Optional QLab version (4 or 5) for version-specific properties 39 | 40 | Returns: 41 | Property configuration dict or None if not found 42 | """ 43 | property_name = property_name.lower() 44 | 45 | # Check global properties first 46 | if property_name in self.global_properties: 47 | return self.global_properties[property_name] 48 | 49 | # Check cue-type-specific properties 50 | if cue_type: 51 | cue_type = cue_type.lower() 52 | 53 | # Handle network cues with version-specific properties 54 | if cue_type == 'network' and qlab_version: 55 | qlab_key = f'qlab{qlab_version}' 56 | if qlab_key in self.cue_type_properties.get('network', {}): 57 | network_props = self.cue_type_properties['network'][qlab_key] 58 | if property_name in network_props: 59 | return network_props[property_name] 60 | 61 | # Check other cue-type properties 62 | if cue_type in self.cue_type_properties: 63 | if property_name in self.cue_type_properties[cue_type]: 64 | return self.cue_type_properties[cue_type][property_name] 65 | 66 | return None 67 | 68 | def build_osc_message(self, property_name, value, cue_type=None, qlab_version=None, cue_data=None): 69 | """ 70 | Build an OSC message for a property 71 | 72 | Args: 73 | property_name: The property name (CSV column header) 74 | value: The value to set 75 | cue_type: Optional cue type for cue-specific properties 76 | qlab_version: Optional QLab version for version-specific properties 77 | cue_data: Optional full cue data dict for conditional properties 78 | 79 | Returns: 80 | OscMessageBuilder or None if property not found 81 | """ 82 | if not value: 83 | return None 84 | 85 | prop_config = self.get_property_config(property_name, cue_type, qlab_version) 86 | 87 | if not prop_config: 88 | return None 89 | 90 | # Check conditions if present 91 | if 'condition' in prop_config and cue_data: 92 | condition = prop_config['condition'] 93 | condition_field = condition.get('field') 94 | condition_value = condition.get('value') 95 | 96 | if cue_data.get(condition_field) != condition_value: 97 | return None 98 | 99 | # Validate value if validation rules exist 100 | if 'valid_range' in prop_config: 101 | try: 102 | int_value = int(value) 103 | min_val, max_val = prop_config['valid_range'] 104 | if int_value < min_val or int_value > max_val: 105 | return None 106 | except ValueError: 107 | return None 108 | 109 | if 'valid_values' in prop_config: 110 | if value.lower() not in [v.lower() for v in prop_config['valid_values']]: 111 | return None 112 | 113 | # Build the message 114 | msg = osc_message_builder.OscMessageBuilder(address=prop_config['osc_address']) 115 | 116 | # Add argument based on type 117 | value_type = prop_config.get('type', 'string') 118 | 119 | try: 120 | if value_type == 'int': 121 | msg.add_arg(int(value)) 122 | elif value_type == 'float': 123 | msg.add_arg(float(value)) 124 | elif value_type == 'bool': 125 | # Handle various boolean representations 126 | bool_value = value.lower() in ['true', '1', 'yes', 'on'] if isinstance(value, str) else bool(value) 127 | msg.add_arg(bool_value) 128 | else: # string 129 | msg.add_arg(str(value)) 130 | except (ValueError, AttributeError): 131 | return None 132 | 133 | return msg 134 | 135 | def get_auto_properties(self, property_name, cue_type=None): 136 | """ 137 | Get properties that should be automatically set when a property is set 138 | 139 | For example, when fadeopacity is set, doopacity should also be set to true 140 | 141 | Returns: 142 | List of (property_name, value) tuples 143 | """ 144 | auto_props = [] 145 | 146 | if cue_type and cue_type in self.cue_type_properties: 147 | cue_props = self.cue_type_properties[cue_type] 148 | 149 | # Check if this property has any related auto properties 150 | for prop_key, prop_config in cue_props.items(): 151 | if 'auto_value' in prop_config and prop_key != property_name: 152 | # Check if this auto property is related to the current property 153 | # For now, we'll handle the specific case of fadeopacity -> doopacity 154 | if property_name == 'fadeopacity' and prop_key == 'doopacity': 155 | auto_props.append((prop_key, prop_config['auto_value'])) 156 | 157 | return auto_props 158 | 159 | 160 | # Singleton instance 161 | _osc_config_instance = None 162 | 163 | def get_osc_config(): 164 | """Get or create the singleton OSC config instance""" 165 | global _osc_config_instance 166 | if _osc_config_instance is None: 167 | _osc_config_instance = OSCConfig() 168 | return _osc_config_instance 169 | -------------------------------------------------------------------------------- /website/docs/reference/csv-columns.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # CSV Column Reference 6 | 7 | This document lists all available CSV columns for creating QLab cues. 8 | 9 | :::info 10 | This documentation is automatically generated from the OSC configuration file. 11 | ::: 12 | 13 | ## Required Columns 14 | 15 | All CSV files **must** include these three columns: 16 | 17 | | Column | Description | Example | 18 | |--------|-------------|---------| 19 | | Number | Cue number | `1`, `LX 12`, `Q100` | 20 | | Type | Cue type | `audio`, `video`, `midi`, `network` | 21 | | Name | Cue name | `Cue 1 GO`, `Main Lights Up` | 22 | 23 | ## Global Properties (All Cue Types) 24 | 25 | These columns can be used with any cue type: 26 | 27 | | CSV Column Header | Description | Type | Valid Values | 28 | |------------------|-------------|------|--------------| 29 | | Armed | Armed state | bool | `true`, `false` | 30 | | Auto Load | Auto-load state | bool | `true`, `false` | 31 | | Color | Cue color | string | none, berry, blue, crimson, cyan, forest, gray, green, hot pink, indigo, lavender, magenta, midnight, olive, orange, peach, plum, purple, red, sky blue, yellow | 32 | | Continue Mode | Continue mode | int | 0=No continue, 1=Auto-continue, 2=Auto-follow | 33 | | Duration | Cue duration | int | Time in seconds | 34 | | File Target | File target path | string | Full path, ~/path, or relative path | 35 | | Flagged | Flagged state | bool | `true`, `false` | 36 | | Follow | Follow mode (alias for Continue Mode) | int | 0-2 | 37 | | Group Mode | Group mode | int | 0=List, 1=Start first and enter, 2=Start first, 3=Timeline, 4=Start random, 5=Cart, 6=Playlist | 38 | | Name | Cue name | string | Any text | 39 | | Notes | Cue notes | string | Any text | 40 | | Number | Cue number | string | Any text/number | 41 | | Post Wait | Post-wait time | int | Time in seconds | 42 | | Pre Wait | Pre-wait time | int | Time in seconds | 43 | | Target | Target cue number | string | Must reference existing cue | 44 | 45 | ## Cue-Type Specific Properties 46 | 47 | ### Audio Cues 48 | 49 | | CSV Column Header | Description | Type | Valid Values | 50 | |------------------|-------------|------|--------------| 51 | | End Time | End time in seconds | float | Decimal number | 52 | | Gang | Level gang/group name | string | Gang name | 53 | | Infinite Loop | Infinite loop state | bool | `true`, `false` | 54 | | Level | Audio level/volume in dB | float | Typically -60 to 12 | 55 | | Patch | Audio patch number | int | 1-16 | 56 | | Pitch | Preserve pitch when rate changes | bool | `true`, `false` | 57 | | Play Count | Number of times to play | int | Positive integer | 58 | | Rate | Playback rate | float | 0.03 to 33.0 | 59 | | Start Time | Start time in seconds | float | Decimal number | 60 | 61 | ### Fade Cues 62 | 63 | | CSV Column Header | Description | Type | Valid Values | 64 | |------------------|-------------|------|--------------| 65 | | Do Fade | Enable general fading | bool | `true`, `false` | 66 | | Fade And Stop Others | Fade and stop mode | int | 0=None, 1=Peers, 2=List/cart, 3=All | 67 | | Fade And Stop Others Time | Fade and stop others time | float | Time in seconds | 68 | | Fade Opacity | Fade opacity | int | 0-1 | 69 | | Stop Target When Done | Stop target when fade is done | bool | `true`, `false` | 70 | 71 | :::tip Auto-Properties 72 | When you set `Fade Opacity`, the `Do Opacity` checkbox is automatically enabled. 73 | ::: 74 | 75 | ### Mic Cues 76 | 77 | | CSV Column Header | Description | Type | Valid Values | 78 | |------------------|-------------|------|--------------| 79 | | Level | Audio level/volume in dB | float | Typically -60 to 12 | 80 | | Patch | Audio patch number | int | 1-16 | 81 | 82 | ### MIDI Cues 83 | 84 | | CSV Column Header | Description | Type | Valid Values | 85 | |------------------|-------------|------|--------------| 86 | | MIDI Command | MIDI command | int | 0-127 | 87 | | MIDI Command Format | MIDI command format | int | 0-127 | 88 | | MIDI Control Number | MIDI control number | int | 0-16383 | 89 | | MIDI Control Value | MIDI control value | int | 0-16383 | 90 | | MIDI Device ID | MIDI device ID | int | 0-127 | 91 | | MIDI Message Type | MIDI message type | int | 1=Voice, 2=MSC, 3=SysEx | 92 | | MIDI Patch Name | MIDI patch name | string | Patch name from workspace | 93 | | MIDI Patch Number | MIDI patch number | int | Index in workspace settings | 94 | | MIDI Q List | MIDI Q list | string | MSC cue list number | 95 | | MIDI Q Number | MIDI Q number | string | MSC cue number | 96 | | MIDI Raw String | MIDI SysEx raw string | string | Hex string (no F0/F7) | 97 | | MIDI Status | MIDI status | int | 0=Note Off, 1=Note On, 2=Key Pressure, 3=Control Change, 4=Program Change, 5=Channel Pressure, 6=Pitch Bend | 98 | 99 | :::info MIDI Resources 100 | See [QLab MIDI Reference](https://qlab.app/docs/v5/scripting/parameter-reference/#midi-show-control-commands) for command details. 101 | ::: 102 | 103 | ### Network Cues 104 | 105 | Network cues work differently in QLab 4 vs QLab 5. Choose the columns based on your QLab version. 106 | 107 | #### QLab 5 Properties 108 | 109 | | CSV Column Header | Description | Type | 110 | |------------------|-------------|------| 111 | | Custom String | Custom string for OSC message or plain text | string | 112 | | Network Patch Name | Network patch name | string | 113 | | Network Patch Number | Network patch number | int | 114 | 115 | :::tip QLab 5 Custom Strings 116 | Use spreadsheet formulas to craft complex OSC messages in the Custom String column. 117 | ::: 118 | 119 | #### QLab 4 Properties 120 | 121 | | CSV Column Header | Description | Type | 122 | |------------------|-------------|------| 123 | | Command | QLab command | int | 124 | | Message Type | Message type | int | 125 | | OSC Cue Number | OSC cue number | string | 126 | | Raw String | Raw OSC string | string | 127 | 128 | :::note 129 | QLab 4 support is maintained but may be deprecated in future releases. 130 | ::: 131 | 132 | ### Text Cues 133 | 134 | | CSV Column Header | Description | Type | Valid Values | 135 | |------------------|-------------|------|--------------| 136 | | Text | Text content for text cue | string | Any text | 137 | 138 | ### Video Cues 139 | 140 | | CSV Column Header | Description | Type | Valid Values | 141 | |------------------|-------------|------|--------------| 142 | | End Time | End time in seconds | float | Decimal number | 143 | | Infinite Loop | Infinite loop state | bool | `true`, `false` | 144 | | Level | Video audio level/volume in dB | float | Typically -60 to 12 | 145 | | Patch | Video patch number | int | 1-16 | 146 | | Play Count | Number of times to play | int | Positive integer | 147 | | Rate | Playback rate | float | 0.03 to 33.0 | 148 | | Stage Number | Video stage number | int | Stage index from workspace settings | 149 | | Start Time | Start time in seconds | float | Decimal number | 150 | 151 | :::tip Video Stages 152 | Stages are only available in QLab 5. 153 | ::: 154 | 155 | ## Valid Cue Types 156 | 157 | Use these values in the `Type` column: 158 | 159 | - `audio` 160 | - `mic` 161 | - `video` 162 | - `camera` 163 | - `text` 164 | - `light` 165 | - `fade` 166 | - `network` 167 | - `midi` 168 | - `midi file` 169 | - `timecode` 170 | - `group` 171 | - `start` 172 | - `stop` 173 | - `pause` 174 | - `load` 175 | - `reset` 176 | - `devamp` 177 | - `goto` 178 | - `target` 179 | - `arm` 180 | - `disarm` 181 | - `wait` 182 | - `memo` 183 | - `script` 184 | - `list`, `cuelist`, `cue list` 185 | - `cart`, `cuecart`, `cue cart` 186 | 187 | ## See Also 188 | 189 | - [Prepare CSV File Tutorial](../tutorial-basics/prepare-csv-file.md) 190 | - [OSC Configuration Schema](../developer/osc-config-schema.md) 191 | -------------------------------------------------------------------------------- /website/docs/developer/building-releases.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Building Releases 6 | 7 | This guide covers building distributable versions of CSV to QLab for macOS using PyInstaller. 8 | 9 | :::info Intended Audience 10 | This guide is for maintainers and contributors who need to build release versions of the GUI application. End users should download pre-built releases from [GitHub Releases](https://github.com/fross123/csv_to_qlab/releases). 11 | ::: 12 | 13 | ## Prerequisites 14 | 15 | ### System Requirements 16 | - macOS (for building .app bundles) 17 | - Python 3.8 or later 18 | - Git 19 | 20 | ### Development Setup 21 | 22 | 1. **Clone the repository:** 23 | ```bash 24 | git clone https://github.com/fross123/csv_to_qlab.git 25 | cd csv_to_qlab 26 | ``` 27 | 28 | 2. **Create and activate virtual environment:** 29 | ```bash 30 | python3 -m venv env 31 | source env/bin/activate 32 | ``` 33 | 34 | 3. **Install dependencies:** 35 | ```bash 36 | pip install -r requirements.txt 37 | pip install pyinstaller 38 | ``` 39 | 40 | ## Building with PyInstaller 41 | 42 | ### Understanding `application.spec` 43 | 44 | The `application.spec` file defines how PyInstaller bundles the application. Key sections: 45 | 46 | **Entry Point** (line 6): 47 | ```python 48 | a = Analysis(['run_gui.py'], # Entry point outside app/ package 49 | ``` 50 | 51 | :::info Entry Point Architecture 52 | The GUI uses `run_gui.py` as its entry point (located in project root), which imports the `app` package. This follows PyInstaller best practices for Python packages with relative imports. 53 | ::: 54 | 55 | **Data Files** (lines 9-12): 56 | ```python 57 | datas=[ 58 | ('app/static', 'static'), # Web UI assets 59 | ('app/templates', 'templates'), # Flask templates 60 | ('app/qlab_osc_config.json', '.'), # OSC configuration (REQUIRED!) 61 | ] 62 | ``` 63 | 64 | **Application Settings** (lines 42-57): 65 | - **name**: `csv-to-qlab.app` 66 | - **icon**: `icon.icns` (must be in root directory) 67 | - **console**: `False` (GUI application, not terminal) 68 | 69 | :::warning Critical Files 70 | The `qlab_osc_config.json` file **must** be included in the bundle, or the application won't be able to send OSC messages to QLab! 71 | ::: 72 | 73 | ### Build Process 74 | 75 | 1. **Verify you're in the project root:** 76 | ```bash 77 | pwd # Should show .../csv_to_qlab 78 | ``` 79 | 80 | 2. **Run PyInstaller:** 81 | ```bash 82 | pyinstaller application.spec 83 | ``` 84 | 85 | 3. **Build output:** 86 | - **`dist/csv-to-qlab/`** - Intermediate build files 87 | - **`dist/csv-to-qlab.app/`** - Final macOS application bundle 88 | 89 | 4. **Build artifacts:** 90 | - **`build/`** - Temporary build files (can be deleted) 91 | - **`dist/`** - Final application bundle 92 | 93 | ### Testing the Build 94 | 95 | Before distributing, thoroughly test the bundled application: 96 | 97 | 1. **Launch the app:** 98 | ```bash 99 | open dist/csv-to-qlab.app 100 | ``` 101 | 102 | 2. **Verify functionality:** 103 | - Application launches without errors 104 | - All UI elements render correctly 105 | - File upload works 106 | - OSC messages send to QLab successfully 107 | - Error handling works properly 108 | 109 | 3. **Test with example files:** 110 | ```bash 111 | # Use the example CSV files in app/static/example_file/ 112 | ``` 113 | 114 | 4. **Check for missing resources:** 115 | - Look for "file not found" errors in Console.app 116 | - Verify all static assets load 117 | - Confirm templates render 118 | 119 | ### Common Build Issues 120 | 121 | #### Missing Icon File 122 | **Error:** `FileNotFoundError: icon.icns` 123 | 124 | **Solution:** Ensure `icon.icns` exists in the project root: 125 | ```bash 126 | ls icon.icns # Should show the file 127 | ``` 128 | 129 | #### Missing OSC Config 130 | **Symptom:** Application launches but fails to send cues 131 | 132 | **Solution:** Verify `qlab_osc_config.json` is in the bundle: 133 | ```bash 134 | # Check bundle contents 135 | ls dist/csv-to-qlab.app/Contents/MacOS/qlab_osc_config.json 136 | ``` 137 | 138 | If missing, check `application.spec` line 12. 139 | 140 | #### Import Errors 141 | **Error:** `ModuleNotFoundError` when running bundled app 142 | 143 | **Solution:** Add missing modules to `hiddenimports` in `application.spec`: 144 | ```python 145 | hiddenimports=['module_name'], 146 | ``` 147 | 148 | ## Creating Distribution Packages 149 | 150 | ### DMG Creation (macOS) 151 | 152 | For official releases, package the .app into a DMG: 153 | 154 | 1. **Install create-dmg** (if not already installed): 155 | ```bash 156 | brew install create-dmg 157 | ``` 158 | 159 | 2. **Create DMG:** 160 | ```bash 161 | create-dmg \ 162 | --volname "CSV to QLab" \ 163 | --window-pos 200 120 \ 164 | --window-size 600 400 \ 165 | --icon-size 100 \ 166 | --app-drop-link 425 120 \ 167 | "CSV-To-QLab.dmg" \ 168 | "dist/csv-to-qlab.app" 169 | ``` 170 | 171 | 3. **Test the DMG:** 172 | - Mount the DMG 173 | - Drag app to Applications 174 | - Launch and verify functionality 175 | 176 | ### Multi-Architecture Builds 177 | 178 | For distributing to both Intel and Apple Silicon Macs: 179 | 180 | **Intel Mac (x86_64):** 181 | ```bash 182 | arch -x86_64 pyinstaller application.spec 183 | ``` 184 | 185 | **Apple Silicon (ARM):** 186 | ```bash 187 | arch -arm64 pyinstaller application.spec 188 | ``` 189 | 190 | :::tip Build on Target Architecture 191 | For best compatibility, build on the target architecture: 192 | - Build Intel version on Intel Mac (or with Rosetta) 193 | - Build ARM version on Apple Silicon Mac 194 | ::: 195 | 196 | ## Release Checklist 197 | 198 | Before publishing a release: 199 | 200 | - [ ] All tests passing (`pytest`) 201 | - [ ] Version number updated in relevant files 202 | - [ ] CHANGELOG.md updated 203 | - [ ] Application builds without errors 204 | - [ ] Bundled app tested on clean macOS installation 205 | - [ ] Example CSV files work correctly 206 | - [ ] Documentation updated 207 | - [ ] Release notes written 208 | - [ ] DMG created and tested 209 | - [ ] GitHub release created with DMG attached 210 | 211 | ## GitHub Actions (Future) 212 | 213 | :::note Automation Opportunity 214 | Consider setting up GitHub Actions to automatically build releases for multiple architectures when tags are pushed. See `.github/workflows/` for existing CI/CD setup. 215 | ::: 216 | 217 | ## Troubleshooting Build Issues 218 | 219 | ### Clean Build 220 | If you encounter persistent issues, try a clean build: 221 | 222 | ```bash 223 | # Remove build artifacts 224 | rm -rf build dist *.spec~ 225 | 226 | # Rebuild 227 | pyinstaller application.spec 228 | ``` 229 | 230 | ### Verbose Output 231 | For debugging build issues: 232 | 233 | ```bash 234 | pyinstaller --log-level DEBUG application.spec 235 | ``` 236 | 237 | ### Check Dependencies 238 | Ensure all dependencies are properly installed: 239 | 240 | ```bash 241 | pip list | grep -E "Flask|pywebview|python-osc" 242 | ``` 243 | 244 | ## Development Workflow 245 | 246 | For rapid development and testing: 247 | 248 | 1. **Use the source directly** (not bundled): 249 | ```bash 250 | python run_gui.py 251 | ``` 252 | 253 | Or with GUI dependencies: 254 | ```bash 255 | pip install -e .[gui] 256 | python run_gui.py 257 | ``` 258 | 259 | 2. **Only build when:** 260 | - Testing distribution-specific issues 261 | - Preparing for release 262 | - Verifying bundling of new resources 263 | 264 | 3. **Use editable install for CLI development:** 265 | ```bash 266 | pip install -e . 267 | ``` 268 | 269 | ## Further Reading 270 | 271 | - [PyInstaller Documentation](https://pyinstaller.org/en/stable/) 272 | - [PyInstaller macOS Bundle](https://pyinstaller.org/en/stable/usage.html#macos-specific-options) 273 | - [Code Signing macOS Apps](https://developer.apple.com/developer-id/) (for official distribution) 274 | 275 | ## Questions? 276 | 277 | If you encounter issues building releases, please: 278 | 1. Check this documentation 279 | 2. Search [existing issues](https://github.com/fross123/csv_to_qlab/issues) 280 | 3. Open a [new issue](https://github.com/fross123/csv_to_qlab/issues/new) with build logs 281 | -------------------------------------------------------------------------------- /website/docs/developer/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Architecture Overview 6 | 7 | CSV to QLab is built with a configuration-driven architecture that makes it easy to add support for new QLab OSC properties without modifying code. 8 | 9 | ## System Architecture 10 | 11 | ``` 12 | CSV File Upload 13 | ↓ 14 | CSV Parser (csv_parser.py) 15 | ↓ 16 | OSC Config Loader (osc_config.py) 17 | ↓ 18 | OSC Message Builder 19 | ↓ 20 | UDP Client (python-osc) → QLab (port 53000) 21 | ↓ 22 | OSC Server (port 53001) ← QLab Replies 23 | ↓ 24 | Error/Success Handler 25 | ↓ 26 | User Feedback (Flask UI) 27 | ``` 28 | 29 | ## Core Components 30 | 31 | ### 1. Entry Points 32 | 33 | **GUI Entry Point (`run_gui.py`):** 34 | - Entry point for PyInstaller GUI builds 35 | - Imports `app.application` as a module 36 | - Creates PyWebView window 37 | - Located in project root (outside `app/` package) 38 | 39 | **CLI Entry Point (`app/cli.py`):** 40 | - Command-line interface entry point 41 | - Installed via `pip install .` as `csv-to-qlab` command 42 | - Provides automation and scripting capabilities 43 | 44 | ### 2. Flask Application (`app/application.py`) 45 | - Web server providing the UI 46 | - Runs inside a PyWebView native window 47 | - Handles file uploads and form submission 48 | - Routes: `/` (upload), `/success` (results) 49 | - Uses relative imports as part of the `app` package 50 | 51 | ### 3. CSV Parser (`app/csv_parser.py`) 52 | The main processing pipeline: 53 | 54 | 1. **Parse CSV** - Reads CSV file into list of dictionaries 55 | 2. **Normalize headers** - Converts to lowercase, removes spaces 56 | 3. **Validate cue types** - Checks against valid types in config 57 | 4. **Build OSC bundles** - For each cue: 58 | - Create `/new {cue_type}` message 59 | - Build property messages using config 60 | - Handle auto-properties (e.g., fadeopacity → doopacity) 61 | 5. **Send to QLab** - UDP transmission with async reply handling 62 | 63 | ### 4. OSC Configuration System (`app/osc_config.py`) 64 | 65 | **Configuration-Driven Design:** 66 | - All OSC properties defined in `qlab_osc_config.json` 67 | - No hardcoded OSC addresses in business logic 68 | - Easy to add new properties or cue types 69 | 70 | **Key Methods:** 71 | ```python 72 | get_property_config(property_name, cue_type, qlab_version) 73 | # Returns config for a property 74 | 75 | build_osc_message(property_name, value, cue_type, qlab_version) 76 | # Builds OSC message from config 77 | 78 | get_auto_properties(property_name, cue_type) 79 | # Returns properties to auto-enable 80 | ``` 81 | 82 | ### 5. OSC Server (`app/osc_server.py`) 83 | - Async UDP server listening on port 53001 84 | - Receives QLab reply messages 85 | - Parses JSON responses for status 86 | - Routes to error/success handlers 87 | 88 | ## Package Structure 89 | 90 | CSV to QLab follows Python best practices for a pip-installable package with PyInstaller GUI: 91 | 92 | ``` 93 | csv_to_qlab/ 94 | ├── run_gui.py # GUI entry point (PyInstaller) 95 | ├── setup.py # pip installation config 96 | ├── application.spec # PyInstaller build config 97 | └── app/ # Main Python package 98 | ├── __init__.py # Package marker 99 | ├── cli.py # CLI entry point 100 | ├── application.py # Flask app 101 | ├── csv_parser.py # CSV processing 102 | ├── osc_config.py # OSC configuration 103 | ├── osc_server.py # OSC server 104 | ├── helper.py # Utilities 105 | ├── error_success_handler.py # Error tracking 106 | ├── qlab_osc_config.json # OSC property definitions 107 | └── tests/ # Test suite 108 | ``` 109 | 110 | **Import Structure:** 111 | - All modules within `app/` use **relative imports** (e.g., `from .csv_parser import send_csv`) 112 | - Entry points (`run_gui.py`, `setup.py`) use **absolute imports** (e.g., `from app.cli import main`) 113 | - This follows PEP 8 and PyInstaller best practices 114 | 115 | ### 6. PyWebView Desktop Wrapper 116 | - Creates native macOS app window 117 | - Frameless design (300x465px) 118 | - Bundles with PyInstaller for distribution 119 | 120 | ## Data Flow Example 121 | 122 | **CSV Input:** 123 | ```csv 124 | Number,Type,Name,Follow,Color 125 | 1,audio,Main Music,2,blue 126 | ``` 127 | 128 | **Processing:** 129 | 1. Parse: `{'number': '1', 'type': 'audio', 'name': 'Main Music', 'follow': '2', 'color': 'blue'}` 130 | 2. Build Bundle: 131 | - `/new` → `"audio"` 132 | - `/cue/selected/number` → `"1"` 133 | - `/cue/selected/name` → `"Main Music"` 134 | - `/cue/selected/continueMode` → `2` (from config: follow → continueMode) 135 | - `/cue/selected/colorName` → `"blue"` 136 | 3. Send bundle via UDP 137 | 4. Receive reply: `{"status": "ok", "workspace_id": "..."}` 138 | 5. Display success 139 | 140 | ## Configuration Schema 141 | 142 | ### Global Properties 143 | Available for all cue types: 144 | ```json 145 | { 146 | "property_name": { 147 | "osc_address": "/cue/selected/...", 148 | "type": "int|float|bool|string", 149 | "description": "Human-readable description", 150 | "valid_range": [min, max], // optional 151 | "valid_values": ["...", "..."] // optional 152 | } 153 | } 154 | ``` 155 | 156 | ### Cue-Type Properties 157 | Specific to certain cue types: 158 | ```json 159 | { 160 | "cue_type_properties": { 161 | "audio": { 162 | "level": { 163 | "osc_address": "/cue/selected/level", 164 | "type": "float" 165 | } 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | ### Version-Specific Properties 172 | Handle QLab 4 vs 5 differences: 173 | ```json 174 | { 175 | "network": { 176 | "qlab5": { /* v5 properties */ }, 177 | "qlab4": { /* v4 properties */ } 178 | } 179 | } 180 | ``` 181 | 182 | ## Auto-Property System 183 | 184 | Some properties automatically enable related settings. For example: 185 | 186 | **User sets:** `Fade Opacity = 0.5` 187 | 188 | **System automatically adds:** 189 | - `Do Opacity = true` (enables the opacity checkbox) 190 | 191 | This is configured with `"auto_value": true` in the JSON config. 192 | 193 | ## Validation 194 | 195 | The system validates: 196 | 1. **Cue types** - Must be in `valid_cue_types` array 197 | 2. **Property ranges** - Checked against `valid_range` if specified 198 | 3. **Property values** - Checked against `valid_values` if specified 199 | 4. **Conditional properties** - Only set if condition is met 200 | 201 | Invalid values are silently skipped (message not sent). 202 | 203 | ## Error Handling 204 | 205 | **Global Error/Success Tracking:** 206 | - `error_success_handler.py` maintains global lists 207 | - Each OSC reply is categorized as success or error 208 | - Displayed to user after all cues processed 209 | 210 | **OSC Reply Format:** 211 | ```json 212 | { 213 | "workspace_id": "ABC123", 214 | "address": "/new", 215 | "status": "ok" // or error message 216 | } 217 | ``` 218 | 219 | ## Adding New Features 220 | 221 | To add support for a new QLab property: 222 | 223 | 1. **No code changes needed!** 224 | 2. Add property to `app/qlab_osc_config.json` 225 | 3. Documentation auto-generated from config 226 | 227 | See [Adding Properties Guide](./adding-properties.md) for details. 228 | 229 | ## Technology Stack 230 | 231 | | Component | Technology | Purpose | 232 | |-----------|-----------|---------| 233 | | Backend | Python 3.9+ | Core logic | 234 | | Web Framework | Flask 3.0.3 | HTTP server | 235 | | Desktop UI | PyWebView 5.1 | Native window | 236 | | OSC Protocol | python-osc 1.8.3 | QLab communication | 237 | | Build Tool | PyInstaller | macOS app bundling | 238 | | Docs | Docusaurus | Documentation site | 239 | 240 | ## QLab Communication 241 | 242 | **Ports:** 243 | - `53000` - Send OSC messages to QLab (UDP) 244 | - `53001` - Receive QLab replies (UDP) 245 | - `53535` - QLab's plain text OSC listener (not used) 246 | 247 | **Connection Flow:** 248 | 1. Send `/connect {passcode}` if provided 249 | 2. Send `/alwaysReply 1` to enable replies (implicit) 250 | 3. Send cue creation bundles 251 | 4. Receive status replies 252 | 5. Close connection (auto-timeout after 61s on UDP) 253 | 254 | ## Design Principles 255 | 256 | 1. **Configuration over Code** - Properties defined in JSON, not Python 257 | 2. **Fail Gracefully** - Invalid properties skipped, processing continues 258 | 3. **Minimal Dependencies** - Small footprint, quick startup 259 | 4. **Type Safety** - Validation at config level 260 | 5. **Version Agnostic** - Same codebase supports QLab 4 & 5 261 | -------------------------------------------------------------------------------- /website/docs/tutorial-basics/prepare-csv-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Prepare a CSV File 6 | 7 | ## Examples 8 | - [Full Example Spreadsheet](https://github.com/fross123/csv_to_qlab/blob/main/app/static/example_file/example.csv) 9 | 10 | - [Simple Example Spreadsheet](https://github.com/fross123/csv_to_qlab/blob/main/app/static/example_file/simple.csv) 11 | 12 | 13 | ## Required columns 14 | 15 | | Number | Type | Name | 16 | | ------ | ------ | ------ | 17 | | 12 | start | Cue 12 GO | 18 | 19 | ---- 20 | 21 | ## Global Properties (All Cue Types) 22 | 23 | These columns work with any cue type: 24 | 25 | #### Notes 26 | Anything you would like to go in the "Notes" area of the cue. 27 | 28 | #### Follow 29 | Continue mode for the cue: 30 | - 0 - No Follow 31 | - 1 - Auto-Continue 32 | - 2 - Auto-Follow 33 | 34 | :::tip 35 | 0, 1, 2 are the only options and the data must be a single number. 36 | ::: 37 | 38 | #### Color 39 | The color of the cue. Available colors: none, berry, blue, crimson, cyan, forest, gray, green, hot pink, indigo, lavender, magenta, midnight, olive, orange, peach, plum, purple, red, sky blue, yellow. 40 | 41 | See [QLab's Color Options](https://qlab.app/docs/v5/scripting/osc-dictionary-v5/#cuecue_numbercolorname-string) for details. 42 | 43 | #### Target 44 | The cue's target. The cue being targeted must be above the cue being created. 45 | 46 | #### File Target 47 | The location of assets for QLab to retrieve. 48 | 49 | Available types: 50 | - Full paths, e.g. /Volumes/MyDisk/path/to/some/file.wav 51 | - Paths beginning with a tilde, e.g. ~/path/to some/file.mov 52 | - Relative paths, e.g. this/is/a/relative/path.mid 53 | - Paths beginning with a tilde (~) will be expanded; the tilde signifies "relative to the user's home directory". 54 | 55 | #### Armed 56 | Set the armed state: `true` or `false` 57 | 58 | #### Flagged 59 | Flag a cue: `true` or `false` 60 | 61 | #### Auto Load 62 | Enable auto-load: `true` or `false` 63 | 64 | #### Duration 65 | Cue duration in seconds 66 | 67 | #### Pre Wait 68 | Pre-wait time in seconds 69 | 70 | #### Post Wait 71 | Post-wait time in seconds 72 | 73 | ---- 74 | 75 | ## Cue types with additional options 76 | 77 | ### Group Cues 78 | 79 | #### Group Mode 80 | :::info 81 | Pre-Release - "Group Mode" is only available when run from source code. 82 | ::: 83 | 84 | | Number | Type | Name | Group Mode | Notes 85 | | ------ | ------ | ------ | ------ | ------ | 86 | | G1 | group | Group Cue 1 | 3 | This would create a timeline group cue 87 | | G2 | group | Group Cue 2 | 6 | This would create a playlist group 88 | 89 | [Options](https://qlab.app/docs/v5/scripting/osc-dictionary-v5/#cuecue_numbermode-number): 90 | - 0 - List 91 | - 1 - Start first and enter 92 | - 2 - Start first 93 | - 3 - Timeline 94 | - 4 - Start random 95 | - 6 - Playlist 96 | :::tip 97 | This is not a typo, "6" is for Playlist type. 98 | ::: 99 | 100 | ---- 101 | 102 | ### Text Cues 103 | #### Text 104 | | Number | Type | Name | Text 105 | | ------ | ------ | ------ | ------ 106 | | T1 | text | Text Cue 1 | this text will be added to the text cue 107 | 108 | The text to enter into the text cue. 109 | 110 | ---- 111 | 112 | ### Fade Cues 113 | | Number | Type | Name | Stop Target When Done | Fade Opacity | Fade And Stop Others | Fade And Stop Others Time | Target 114 | | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 115 | | V1 | video | Video Cue 1 | | | | | 116 | | F1 | fade | Fade Cue 1 | false | 0 | 1 | 2.5 | V1 117 | | F2 | fade | Fade Cue 2 | true | 1 | 3 | 1.0 | V1 118 | 119 | #### Stop Target When Done 120 | Stop the target cue when the fade completes: `true` or `false` 121 | 122 | #### Fade Opacity 123 | Fade opacity value (0-1, where 0 is transparent and 1 is opaque) 124 | 125 | :::tip Auto-Enable 126 | Setting Fade Opacity automatically enables the "Do Opacity" checkbox 127 | ::: 128 | 129 | #### Fade And Stop Others 130 | Fade and stop mode: 131 | - 0 - None 132 | - 1 - Peers (cues at same level) 133 | - 2 - List or cart 134 | - 3 - All 135 | 136 | #### Fade And Stop Others Time 137 | Time in seconds for the fade and stop action (decimal values allowed) 138 | 139 | #### Do Fade / Do Volume 140 | Enable fading for general properties or volume: `true` or `false` 141 | 142 | ---- 143 | 144 | ### Audio Cues 145 | 146 | | Number | Type | Name | Level | Rate | Pitch | Infinite Loop | Play Count | Start Time | End Time | Patch | 147 | | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 148 | | A1 | audio | Music Cue | -6.5 | 1.0 | true | false | 1 | 0 | 120.5 | 1 | 149 | 150 | #### Level 151 | Audio level/volume in dB (typically -60 to 12) 152 | 153 | #### Rate 154 | Playback rate (0.03 to 33.0, where 1.0 is normal speed) 155 | 156 | #### Pitch 157 | Preserve pitch when rate changes: `true` or `false` 158 | 159 | #### Infinite Loop 160 | Enable infinite looping: `true` or `false` 161 | 162 | #### Play Count 163 | Number of times to play (integer) 164 | 165 | #### Start Time / End Time 166 | Start and end time in seconds (decimal values allowed) 167 | 168 | #### Patch 169 | Audio patch number (1-16) 170 | 171 | #### Gang 172 | Level gang/group name for linked volume control 173 | 174 | ---- 175 | 176 | ### Video Cues 177 | 178 | | Number | Type | Name | Level | Rate | Stage Number | Infinite Loop | Play Count | Start Time | End Time | Patch | 179 | | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 180 | | V1 | video | Video Cue | -6.5 | 1.0 | 1 | false | 1 | 0 | 120.5 | 1 | 181 | 182 | #### Stage Number 183 | The stage number in order of the list in the "video outputs" setting 184 | 185 | :::tip 186 | Stages are in QLab 5 only. 187 | ::: 188 | 189 | #### Level 190 | Video audio level/volume in dB 191 | 192 | #### Rate 193 | Playback rate (0.03 to 33.0) 194 | 195 | #### Infinite Loop 196 | Enable infinite looping: `true` or `false` 197 | 198 | #### Play Count 199 | Number of times to play 200 | 201 | #### Start Time / End Time 202 | Start and end time in seconds 203 | 204 | #### Patch 205 | Video patch number (1-16) 206 | 207 | ---- 208 | 209 | ### MIDI Cues 210 | | Number | Type | Name | MIDI Message Type | MIDI Q Number | MIDI Q List | MIDI Device ID | MIDI Patch Number |MIDI Control Number | MIDI Control Value | MIDI Raw String 211 | | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 212 | | M1 | midi | MIDI Voice Cue 1 | 1 | | | | 1 | 2 | 10 | 213 | | M2 | midi | MIDI MSC Cue 2 | 2 | 12 | 1 | 3 | 1 | | | 214 | | M3 | midi | MIDI SysEx Cue 3 | 3 | | | | 1 | | | F5 02 215 | 216 | #### MIDI Message Type 217 | - 1 - MIDI Voice Message ("Musical MIDI") 218 | - 2 - MIDI Show Control Message (MSC) 219 | - 3 - MIDI SysEx Message 220 | 221 | #### MIDI Q Number 222 | The number of the cue. Specific to MSC cue types. 223 | 224 | #### MIDI Q List 225 | The Cue List for the MSC cue. 226 | 227 | #### MIDI Device ID 228 | The Device ID of the MSC Cue 229 | 230 | #### MIDI Control Number 231 | 232 | #### MIDI Control Value 233 | 234 | #### MIDI Patch Name 235 | The Name of the MIDI Patch 236 | 237 | #### MIDI Patch Number 238 | The patch of the MIDI cue in order by the workspace settings. Index 1 means the first patch in the patch list in Workspace Settings. 239 | 240 | #### MIDI Raw String 241 | :::info 242 | Pre-Release - "MIDI Raw String" is currently only available when run from source code. 243 | ::: 244 | For Midi SysEx Messages 245 | 246 | #### MIDI Command Format 247 | [Reference QLab Docs](https://qlab.app/docs/v5/scripting/parameter-reference/#midi-show-control-command-format-types) 248 | 249 | #### MIDI Command 250 | [Reference QLab Docs](https://qlab.app/docs/v5/scripting/parameter-reference/#midi-show-control-commands) 251 | 252 | ---- 253 | 254 | ### Network Cues 255 | The way network cues work is slightly different in QLab 4 vs QLab 5 256 | 257 | #### QLab 5 258 | ##### Network Patch Number 259 | The number of the network patch. Based on the list in the workspace settings. 1 is at the top, etc... 260 | 261 | ##### Network Patch Name 262 | The Name of the network patch. 263 | 264 | ##### Custom String 265 | The best way to facilitate the vast amount of commands available in QLab 5 was to use custom string. You should be able to craft desired strings easily using common spreadsheet formulas and tools. 266 | 267 | --- 268 | 269 | #### QLab 4 270 | :::note 271 | There are no plans to remove these features, but we will post here on this site if/when support for QLab 4 ends. 272 | ::: 273 | 274 | ##### Message Type 275 | Reference [QLab Docs](https://qlab.app/docs/v4/scripting/osc-dictionary-v4/#cuecue_numbermessagetype-number) 276 | 277 | ##### OSC Cue Number 278 | Only if using QLab Message Type 279 | 280 | ##### Command 281 | For QLab Messages, review the [QLab Docs](https://qlab.app/docs/v4/scripting/osc-dictionary-v4/#cuecue_numberqlabcommand-number) 282 | 283 | For OSC Messages, you may now include a raw string in the column. 284 | 285 | ---- 286 | 287 | ## See Complete Reference 288 | 289 | For a comprehensive list of all available columns and cue types, see the [CSV Column Reference](../reference/csv-columns.md). 290 | -------------------------------------------------------------------------------- /website/docs/developer/adding-properties.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Adding New OSC Properties 6 | 7 | This guide shows you how to add support for new QLab OSC properties without writing any Python code. 8 | 9 | ## Overview 10 | 11 | All OSC properties are defined in `app/qlab_osc_config.json`. Adding a new property is as simple as adding a JSON entry. 12 | 13 | ## Step-by-Step Guide 14 | 15 | ### 1. Find the OSC Address 16 | 17 | Consult the [QLab OSC Dictionary](https://qlab.app/docs/v5/scripting/osc-dictionary-v5/) to find: 18 | - The OSC address (e.g., `/cue/selected/propertyName`) 19 | - The expected data type (string, number, boolean) 20 | - Valid values or ranges 21 | - Which cue types support it 22 | 23 | ### 2. Determine Property Scope 24 | 25 | Decide if the property is: 26 | - **Global** - Available for all cue types → Add to `global_properties` 27 | - **Cue-Specific** - Only for certain types → Add to `cue_type_properties` 28 | - **Version-Specific** - Different in QLab 4 vs 5 → Use nested structure 29 | 30 | ### 3. Add to Configuration 31 | 32 | #### Example 1: Global Property 33 | 34 | Let's add support for the `armed` property: 35 | 36 | ```json 37 | { 38 | "global_properties": { 39 | "armed": { 40 | "osc_address": "/cue/selected/armed", 41 | "type": "bool", 42 | "description": "Armed state of the cue" 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | **CSV Usage:** 49 | ```csv 50 | Number,Type,Name,Armed 51 | 1,audio,Music Cue,true 52 | ``` 53 | 54 | #### Example 2: Cue-Specific Property 55 | 56 | Add `level` for audio cues: 57 | 58 | ```json 59 | { 60 | "cue_type_properties": { 61 | "audio": { 62 | "level": { 63 | "osc_address": "/cue/selected/level", 64 | "type": "float", 65 | "description": "Audio level/volume in dB" 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | **CSV Usage:** 73 | ```csv 74 | Number,Type,Name,Level 75 | 1,audio,Music Cue,-6.5 76 | ``` 77 | 78 | #### Example 3: Property with Validation 79 | 80 | Add `continueMode` with valid range: 81 | 82 | ```json 83 | { 84 | "global_properties": { 85 | "continuemode": { 86 | "osc_address": "/cue/selected/continueMode", 87 | "type": "int", 88 | "description": "Continue mode (0=No continue, 1=Auto-continue, 2=Auto-follow)", 89 | "valid_range": [0, 2] 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | Values outside 0-2 will be rejected. 96 | 97 | #### Example 4: Property with Valid Values 98 | 99 | Add `color` with specific options: 100 | 101 | ```json 102 | { 103 | "global_properties": { 104 | "color": { 105 | "osc_address": "/cue/selected/colorName", 106 | "type": "string", 107 | "description": "Cue color", 108 | "valid_values": [ 109 | "none", "berry", "blue", "crimson", "cyan", 110 | "forest", "gray", "green", "hot pink", "indigo" 111 | ] 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | Only listed colors will be accepted (case-insensitive). 118 | 119 | #### Example 5: Conditional Property 120 | 121 | Add a property that only applies when another field has a specific value: 122 | 123 | ```json 124 | { 125 | "cue_type_properties": { 126 | "network": { 127 | "qlab4": { 128 | "command": { 129 | "osc_address": "/cue/selected/qlabCommand", 130 | "type": "int", 131 | "description": "QLab command (QLab 4)", 132 | "condition": { 133 | "field": "messagetype", 134 | "value": "1" 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | The `command` property only sends if `messagetype` equals `"1"`. 144 | 145 | ### 4. Auto-Properties 146 | 147 | Some properties automatically enable related settings. For example, setting a fade value should enable its checkbox. 148 | 149 | ```json 150 | { 151 | "cue_type_properties": { 152 | "fade": { 153 | "fadeopacity": { 154 | "osc_address": "/cue/selected/opacity", 155 | "type": "int", 156 | "description": "Fade opacity (0-1)" 157 | }, 158 | "doopacity": { 159 | "osc_address": "/cue/selected/doOpacity", 160 | "type": "bool", 161 | "description": "Enable opacity fading", 162 | "auto_value": true 163 | } 164 | } 165 | } 166 | } 167 | ``` 168 | 169 | When user sets `Fade Opacity`, the system automatically sends `doOpacity: true`. 170 | 171 | **Current auto-property logic** (`osc_config.py:154`): 172 | ```python 173 | if property_name == 'fadeopacity' and prop_key == 'doopacity': 174 | auto_props.append((prop_key, prop_config['auto_value'])) 175 | ``` 176 | 177 | To extend this, modify the `get_auto_properties()` method. 178 | 179 | ### 5. Version-Specific Properties 180 | 181 | Network cues work differently in QLab 4 vs 5: 182 | 183 | ```json 184 | { 185 | "cue_type_properties": { 186 | "network": { 187 | "qlab5": { 188 | "customstring": { 189 | "osc_address": "/cue/selected/customString", 190 | "type": "string", 191 | "description": "Custom string for OSC message (QLab 5)" 192 | } 193 | }, 194 | "qlab4": { 195 | "rawstring": { 196 | "osc_address": "/cue/selected/rawString", 197 | "type": "string", 198 | "description": "Raw OSC string (QLab 4)" 199 | } 200 | } 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | The system automatically picks the right property based on the QLab version parameter. 207 | 208 | ## Property Configuration Options 209 | 210 | ### Required Fields 211 | 212 | | Field | Type | Description | 213 | |-------|------|-------------| 214 | | `osc_address` | string | Full OSC path (e.g., `/cue/selected/name`) | 215 | | `type` | string | Data type: `string`, `int`, `float`, `bool` | 216 | | `description` | string | Human-readable description | 217 | 218 | ### Optional Fields 219 | 220 | | Field | Type | Description | 221 | |-------|------|-------------| 222 | | `valid_range` | array | `[min, max]` for numeric validation | 223 | | `valid_values` | array | List of allowed string values | 224 | | `condition` | object | `{field, value}` - only send if condition met | 225 | | `auto_value` | any | Value to auto-set when this property is triggered | 226 | 227 | ## CSV Column Name Mapping 228 | 229 | CSV headers are automatically normalized: 230 | - Converted to lowercase 231 | - Spaces removed 232 | - Matched against config keys 233 | 234 | **Examples:** 235 | - CSV: `"MIDI Device ID"` → Config: `"midideviceid"` 236 | - CSV: `"Network Patch Number"` → Config: `"networkpatchnumber"` 237 | - CSV: `"Pre Wait"` → Config: `"prewait"` 238 | 239 | ## Testing Your Property 240 | 241 | 1. **Add to config** - Edit `app/qlab_osc_config.json` 242 | 2. **Create test CSV** - Include your new column 243 | 3. **Run app** - `python3 application.py` (from project root) 244 | 4. **Upload CSV** - Test with an empty QLab workspace 245 | 5. **Verify in QLab** - Check that the property was set correctly 246 | 247 | ## Documentation Updates 248 | 249 | After adding properties: 250 | 251 | 1. **Auto-generated reference** - Run to update docs: 252 | ```bash 253 | cd app 254 | python3 generate_column_docs.py > ../website/docs/reference/csv-columns.md 255 | ``` 256 | 257 | 2. **Manual docs** - Update `website/docs/tutorial-basics/prepare-csv-file.md` with examples 258 | 259 | ## Common Mistakes 260 | 261 | ❌ **Wrong CSV header case** 262 | ```csv 263 | midi device id # Won't match 264 | ``` 265 | ✅ **Use exact capitalization or any case (normalized)** 266 | ```csv 267 | MIDI Device ID # Matches "midideviceid" 268 | ``` 269 | 270 | ❌ **Type mismatch** 271 | ```json 272 | {"type": "int"} // Config expects integer 273 | ``` 274 | ```csv 275 | Level,5.5 // CSV has float - will fail 276 | ``` 277 | 278 | ❌ **Missing from valid_values** 279 | ```json 280 | {"valid_values": ["red", "blue"]} 281 | ``` 282 | ```csv 283 | Color,green // Rejected, not in list 284 | ``` 285 | 286 | ## Advanced: Adding New Cue Types 287 | 288 | To add support for a completely new cue type: 289 | 290 | 1. Add to `valid_cue_types` array: 291 | ```json 292 | { 293 | "valid_cue_types": [ 294 | "audio", "video", "midi", "yournewtype" 295 | ] 296 | } 297 | ``` 298 | 299 | 2. Add cue-specific properties: 300 | ```json 301 | { 302 | "cue_type_properties": { 303 | "yournewtype": { 304 | "customproperty": { 305 | "osc_address": "/cue/selected/customProperty", 306 | "type": "string" 307 | } 308 | } 309 | } 310 | } 311 | ``` 312 | 313 | 3. Test with QLab to ensure it accepts the cue type via `/new yournewtype` 314 | 315 | ## Need Help? 316 | 317 | - [QLab OSC Dictionary](https://qlab.app/docs/v5/scripting/osc-dictionary-v5/) 318 | - [OSC Configuration Schema](./osc-config-schema.md) 319 | - [Architecture Overview](./architecture.md) 320 | - [GitHub Issues](https://github.com/fross123/csv_to_qlab/issues) 321 | -------------------------------------------------------------------------------- /website/docs/developer/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Testing Guide 6 | 7 | CSV to QLab uses pytest for testing with comprehensive coverage of the configuration-driven architecture. 8 | 9 | ## Test Suite Overview 10 | 11 | **Total Coverage: 86%** 12 | 13 | The test suite includes: 14 | - 50 total tests 15 | - 28 OSC configuration tests 16 | - 17 CSV parsing tests 17 | - 5 integration tests 18 | 19 | ## Running Tests 20 | 21 | ### Prerequisites 22 | 23 | ```bash 24 | # Activate virtual environment 25 | source env/bin/activate 26 | 27 | # Install test dependencies 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | ### Run All Tests 32 | 33 | ```bash 34 | # Run all tests 35 | python -m pytest 36 | 37 | # Run with verbose output 38 | python -m pytest -v 39 | 40 | # Run with coverage report 41 | python -m pytest --cov=app --cov-report=term-missing 42 | ``` 43 | 44 | ### Run Specific Test Files 45 | 46 | ```bash 47 | # OSC configuration tests only 48 | python -m pytest app/tests/test_osc_config.py -v 49 | 50 | # CSV parser tests only 51 | python -m pytest app/tests/test_csv_parser.py -v 52 | 53 | # Integration tests only 54 | python -m pytest app/tests/test_app.py -v 55 | ``` 56 | 57 | ### Run Specific Test Classes or Functions 58 | 59 | ```bash 60 | # Run specific test class 61 | python -m pytest app/tests/test_osc_config.py::TestDuplicatePropertyNames -v 62 | 63 | # Run specific test function 64 | python -m pytest app/tests/test_osc_config.py::TestDuplicatePropertyNames::test_no_duplicate_property_names_global_vs_cue_specific -v 65 | ``` 66 | 67 | ## Test Files 68 | 69 | ### `test_osc_config.py` 70 | 71 | Tests for OSC configuration loading and validation. 72 | 73 | **Critical Tests:** 74 | - ✅ **Duplicate property name detection** - Prevents silent failures 75 | - ✅ Property validation (ranges, enums, types) 76 | - ✅ Version-specific properties (QLab 4 vs 5) 77 | - ✅ Auto-properties 78 | - ✅ Conditional properties 79 | 80 | **Example:** 81 | ```python 82 | def test_no_duplicate_property_names_global_vs_cue_specific(): 83 | """Verify no property names overlap between global and cue-specific""" 84 | # This test prevents configuration errors where the same property 85 | # name exists in both global_properties and cue_type_properties, 86 | # which would cause the global property to always win in lookups 87 | ``` 88 | 89 | ### `test_csv_parser.py` 90 | 91 | Tests for CSV parsing and OSC message generation. 92 | 93 | **Covered Areas:** 94 | - CSV parsing and header normalization 95 | - Property processing 96 | - Passcode handling 97 | - Cue type validation 98 | - Edge cases (unicode, special characters) 99 | 100 | ### `test_app.py` 101 | 102 | Integration tests for the Flask application. 103 | 104 | **Covered Areas:** 105 | - HTTP endpoints 106 | - File upload handling 107 | - QLab version handling 108 | - Error responses 109 | 110 | ## Critical Tests Explained 111 | 112 | ### 1. Duplicate Property Detection 113 | 114 | **Why it's critical:** If a property name exists in both `global_properties` and a cue type's properties, the global property always wins. This causes silent failures where cue-specific properties are never used. 115 | 116 | **Test Location:** `test_osc_config.py::TestDuplicatePropertyNames` 117 | 118 | **What it checks:** 119 | ```python 120 | # Fails if any property name appears in both: 121 | global_props = {"level": {...}} 122 | cue_type_props = {"audio": {"level": {...}}} # CONFLICT! 123 | ``` 124 | 125 | ### 2. Property Validation 126 | 127 | **Why it's critical:** Invalid values sent to QLab can cause cues to be created with wrong settings. 128 | 129 | **Test Location:** `test_osc_config.py::TestPropertyValidation` 130 | 131 | **What it checks:** 132 | - Numeric ranges (e.g., `continueMode` must be 0-2) 133 | - Valid values lists (e.g., `color` must be from approved list) 134 | - Type conversion (int, float, bool, string) 135 | 136 | ### 3. Version Isolation 137 | 138 | **Why it's critical:** QLab 4 and 5 have different OSC properties, especially for network cues. 139 | 140 | **Test Location:** `test_osc_config.py::TestVersionSpecificProperties` 141 | 142 | **What it checks:** 143 | - QLab 5 `customstring` not available in QLab 4 144 | - QLab 4 `messagetype` not available in QLab 5 145 | - Version-specific properties don't leak across versions 146 | 147 | ## Writing New Tests 148 | 149 | ### Test Structure 150 | 151 | ```python 152 | import pytest 153 | from osc_config import OSCConfig 154 | 155 | class TestNewFeature: 156 | """Test description""" 157 | 158 | def test_feature_works(self): 159 | """Specific test case""" 160 | config = OSCConfig() 161 | result = config.some_method() 162 | assert result is not None 163 | ``` 164 | 165 | ### Using Fixtures 166 | 167 | ```python 168 | @pytest.fixture 169 | def config(): 170 | """Reusable OSC config""" 171 | return OSCConfig() 172 | 173 | def test_with_fixture(config): 174 | assert config is not None 175 | ``` 176 | 177 | ### Mocking External Dependencies 178 | 179 | ```python 180 | from unittest.mock import Mock, patch 181 | 182 | def test_with_mock(): 183 | with patch('csv_parser.udp_client.UDPClient') as mock_client: 184 | mock_client.return_value = Mock() 185 | # Test code that uses UDP client 186 | ``` 187 | 188 | ## Coverage Goals 189 | 190 | | Module | Target | Current | 191 | |--------|--------|---------| 192 | | `osc_config.py` | 95%+ | 94% ✅ | 193 | | `csv_parser.py` | 90%+ | 100% ✅ | 194 | | `application.py` | 80%+ | 82% ✅ | 195 | | Overall | 85%+ | 86% ✅ | 196 | 197 | ## Adding Tests for New Properties 198 | 199 | When adding a new property to `qlab_osc_config.json`: 200 | 201 | ### 1. Add Validation Test 202 | 203 | ```python 204 | def test_new_property_validation(config): 205 | """Test the new property validates correctly""" 206 | msg = config.build_osc_message('newproperty', 'valid_value') 207 | assert msg is not None 208 | 209 | msg = config.build_osc_message('newproperty', 'invalid_value') 210 | assert msg is None 211 | ``` 212 | 213 | ### 2. Update Duplicate Detection Test 214 | 215 | The duplicate detection test automatically checks all properties, so no changes needed if following naming conventions. 216 | 217 | ### 3. Add Type Conversion Test (if applicable) 218 | 219 | ```python 220 | def test_new_property_type_conversion(config): 221 | """Test type conversion for new property""" 222 | # For int properties 223 | msg = config.build_osc_message('newproperty', '5') 224 | assert msg is not None 225 | ``` 226 | 227 | ## Continuous Integration 228 | 229 | Tests run automatically on GitHub Actions for: 230 | - Pull requests 231 | - Pushes to main branch 232 | - Release workflows 233 | 234 | ### CI Configuration 235 | 236 | See `.github/workflows/pytest.yml` for CI setup. 237 | 238 | ## Test Dependencies 239 | 240 | From `requirements.txt`: 241 | - `pytest>=8.0.0` - Testing framework 242 | - `pytest-cov>=4.1.0` - Coverage reporting 243 | 244 | ## Common Test Patterns 245 | 246 | ### Testing Configuration Loading 247 | 248 | ```python 249 | def test_config_loads(): 250 | config = OSCConfig() 251 | assert config.global_properties is not None 252 | assert config.cue_type_properties is not None 253 | ``` 254 | 255 | ### Testing OSC Message Building 256 | 257 | ```python 258 | def test_build_message(): 259 | config = OSCConfig() 260 | msg = config.build_osc_message('name', 'Test Cue') 261 | assert msg is not None 262 | built = msg.build() 263 | assert built is not None 264 | ``` 265 | 266 | ### Testing CSV Parsing 267 | 268 | ```python 269 | from unittest.mock import Mock, patch 270 | 271 | def test_csv_parsing(): 272 | with patch('csv_parser.udp_client.UDPClient') as mock_client: 273 | csv_content = MockFileStorage("Number,Type,Name\n1,audio,Test") 274 | send_csv('127.0.0.1', csv_content, 5, '') 275 | assert mock_client.return_value.send.called 276 | ``` 277 | 278 | ## Debugging Failed Tests 279 | 280 | ### Verbose Output 281 | 282 | ```bash 283 | python -m pytest -vv # Extra verbose 284 | python -m pytest -s # Show print statements 285 | ``` 286 | 287 | ### Run Single Test 288 | 289 | ```bash 290 | python -m pytest app/tests/test_osc_config.py::test_name -vv 291 | ``` 292 | 293 | ### Use pytest.set_trace() for Debugging 294 | 295 | ```python 296 | def test_something(): 297 | import pytest 298 | pytest.set_trace() # Drops into debugger 299 | # Test code 300 | ``` 301 | 302 | ## Test Coverage Reports 303 | 304 | ### Terminal Report 305 | 306 | ```bash 307 | python -m pytest --cov=app --cov-report=term-missing 308 | ``` 309 | 310 | ### HTML Report 311 | 312 | ```bash 313 | python -m pytest --cov=app --cov-report=html 314 | open htmlcov/index.html 315 | ``` 316 | 317 | ### Coverage by Module 318 | 319 | ```bash 320 | python -m pytest --cov=app --cov-report=term-missing | grep app/ 321 | ``` 322 | 323 | ## Best Practices 324 | 325 | 1. **Test behavior, not implementation** - Test what the code does, not how it does it 326 | 2. **Use descriptive test names** - `test_no_duplicate_property_names_global_vs_cue_specific` is better than `test_duplicates` 327 | 3. **One assertion per test** (when possible) - Makes failures easier to debug 328 | 4. **Mock external dependencies** - Don't make real network calls or file I/O in unit tests 329 | 5. **Test edge cases** - Empty strings, unicode, very long values, etc. 330 | 6. **Keep tests fast** - Mock slow operations, avoid sleep() 331 | 332 | ## Troubleshooting 333 | 334 | ### Import Errors 335 | 336 | ```bash 337 | # Make sure you're in the project root 338 | cd /path/to/csv_to_qlab 339 | 340 | # Make sure virtual env is activated 341 | source env/bin/activate 342 | 343 | # Reinstall dependencies 344 | pip install -r requirements.txt 345 | ``` 346 | 347 | ### Coverage Not Working 348 | 349 | ```bash 350 | # Install coverage separately 351 | pip install pytest-cov 352 | 353 | # Check pytest plugins 354 | python -m pytest --version 355 | ``` 356 | 357 | ### Mocking Errors 358 | 359 | ```python 360 | # Use correct import path 361 | # ❌ Wrong: with patch('osc_config') as mock: 362 | # ✅ Right: with patch('csv_parser.get_osc_config') as mock: 363 | ``` 364 | 365 | ## See Also 366 | 367 | - [Architecture Overview](./architecture.md) - System design 368 | - [Adding Properties Guide](./adding-properties.md) - Extend configuration 369 | - [OSC Configuration Schema](./osc-config-schema.md) - JSON structure 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSV to QLab 2 | 3 | [![Tests](https://github.com/fross123/csv_to_qlab/workflows/Tests/badge.svg)](https://github.com/fross123/csv_to_qlab/actions) 4 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 5 | 6 | A tool to send CSV files to QLab via OSC. Available as both a GUI application and command-line interface. 7 | 8 | [📖 Full Documentation](https://fross123.github.io/csv_to_qlab/) | [🐛 Report Bug](https://github.com/fross123/csv_to_qlab/issues) | [💡 Request Feature](https://github.com/fross123/csv_to_qlab/issues) 9 | 10 | ## Features 11 | 12 | ✨ **Dual Interface** - GUI application for Mac and cross-platform CLI 13 | 📝 **Configuration-Driven** - Easy to extend with JSON-based OSC property definitions 14 | 🎯 **Comprehensive Support** - Supports all major QLab cue types and properties 15 | 🤖 **Automation Ready** - CLI with JSON output for scripting and batch processing 16 | ✅ **Well Tested** - 69 tests with 86% code coverage 17 | 📚 **Documented** - Extensive user and developer documentation 18 | 19 | ## Installation 20 | 21 | ### GUI Application (Mac only) 22 | 23 | Download the latest release: 24 | - [macOS 15 ARM](https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS15-ARM.dmg) 25 | - [macOS 14 ARM](https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab-macOS14-ARM.dmg) 26 | - [macOS 11+ Intel](https://github.com/fross123/csv_to_qlab/releases/latest/download/CSV-To-QLab.dmg) 27 | 28 | **Note:** I do not currently have an Apple Developer Certificate, so you'll see security warnings when opening the app. The code is open source and auditable. If you have concerns, you can build from source or use the CLI. 29 | 30 | **Setup:** 31 | 1. Download and open the DMG 32 | 2. Drag the app to your Applications folder 33 | 3. Right-click the app and select "Open" to bypass Gatekeeper 34 | 4. QLab must be open on the receiving computer for messages to be received 35 | 36 | ### Command-Line Interface (Cross-platform) 37 | 38 | The CLI works on Mac, Linux, and Windows - ideal for automation and scripting. 39 | 40 | ```bash 41 | # Clone the repository 42 | git clone https://github.com/fross123/csv_to_qlab.git 43 | cd csv_to_qlab 44 | 45 | # Install CLI-only (recommended) 46 | pip install . 47 | 48 | # Or install with GUI support 49 | pip install .[gui] 50 | ``` 51 | 52 | **Basic Usage:** 53 | ```bash 54 | # Send a CSV file to QLab 55 | csv-to-qlab show.csv 127.0.0.1 5 56 | 57 | # With passcode 58 | csv-to-qlab show.csv 192.168.1.100 5 --passcode 1234 59 | 60 | # JSON output for scripting 61 | csv-to-qlab show.csv 127.0.0.1 5 --json 62 | 63 | # See all options 64 | csv-to-qlab --help 65 | ``` 66 | 67 | **When to Use GUI vs CLI:** 68 | - **GUI**: Quick one-off imports, visual feedback, Mac users 69 | - **CLI**: Automation, scripting, batch processing, remote/SSH sessions, cross-platform 70 | 71 | ## CSV File Format 72 | 73 | ### Required Columns 74 | Every CSV file must have these three columns: 75 | 76 | | Number | Type | Name | 77 | |--------|------|------| 78 | | 12 | audio | Cue 12 GO | 79 | | 13 | video | Video Playback | 80 | 81 | ### Optional Columns 82 | 83 | CSV to QLab supports a wide range of optional properties for all cue types: 84 | 85 | **Global Properties** (all cue types): 86 | - Notes, Color, Follow (Continue Mode) 87 | - Armed, Flagged, Auto Load 88 | - Duration, Pre Wait, Post Wait 89 | - Target, File Target 90 | 91 | **Audio/Video Cues:** 92 | - Level, Rate, Pitch 93 | - Loop, Infinite Loop 94 | - Start Time, End Time 95 | - Patch, Gang 96 | 97 | **MIDI Cues:** 98 | - MIDI Device ID, Message Type 99 | - Control Number, Control Value 100 | - Patch Channel, Patch Number 101 | - MSC Command, Command Format 102 | 103 | **Network Cues:** 104 | - QLab 4: Message Type, OSC Cue Number, Command 105 | - QLab 5: Network Patch Number/Channel, Custom String 106 | 107 | **Fade Cues:** 108 | - Fade opacity, Do opacity 109 | - Fade and Stop Others 110 | 111 | **[📖 Complete CSV Column Reference](https://fross123.github.io/csv_to_qlab/docs/reference/csv-columns)** 112 | 113 | ### Examples 114 | 115 | - [Simple Example](https://github.com/fross123/csv_to_qlab/blob/main/app/static/example_file/simple.csv) 116 | - [Full Example with Multiple Cue Types](https://github.com/fross123/csv_to_qlab/blob/main/app/static/example_file/example.csv) 117 | 118 | ## Documentation 119 | 120 | **User Documentation:** 121 | - [Installation Guide](https://fross123.github.io/csv_to_qlab/docs/tutorial-basics/installation) 122 | - [Preparing CSV Files](https://fross123.github.io/csv_to_qlab/docs/tutorial-basics/prepare-csv-file) 123 | - [Sending to QLab](https://fross123.github.io/csv_to_qlab/docs/tutorial-basics/send-to-qlab) 124 | - [CLI Advanced Usage](https://fross123.github.io/csv_to_qlab/docs/tutorial-basics/cli-advanced) 125 | 126 | **Developer Documentation:** 127 | - [Architecture Overview](https://fross123.github.io/csv_to_qlab/docs/developer/architecture) 128 | - [Adding Properties](https://fross123.github.io/csv_to_qlab/docs/developer/adding-properties) 129 | - [OSC Config Schema](https://fross123.github.io/csv_to_qlab/docs/developer/osc-config-schema) 130 | - [Testing Guide](https://fross123.github.io/csv_to_qlab/docs/developer/testing) 131 | - [Building Releases](https://fross123.github.io/csv_to_qlab/docs/developer/building-releases) 132 | 133 | ## Contributing 134 | 135 | Contributions are welcome! Whether you're fixing bugs, adding features, improving documentation, or adding new cue properties. 136 | 137 | ### Getting Started 138 | 139 | 1. **Fork the repository** 140 | 2. **Clone your fork:** 141 | ```bash 142 | git clone https://github.com/YOUR_USERNAME/csv_to_qlab.git 143 | cd csv_to_qlab 144 | ``` 145 | 146 | 3. **Set up development environment:** 147 | ```bash 148 | # Create virtual environment 149 | python3 -m venv env 150 | source env/bin/activate # On Windows: env\Scripts\activate 151 | 152 | # Install dependencies 153 | pip install -r requirements.txt 154 | 155 | # Install in editable mode 156 | pip install -e . 157 | ``` 158 | 159 | 4. **Create a feature branch:** 160 | ```bash 161 | git checkout -b feature/amazing-feature 162 | ``` 163 | 164 | 5. **Make your changes and test:** 165 | ```bash 166 | # Run tests 167 | pytest 168 | 169 | # Run with coverage 170 | pytest --cov=app --cov-report=html 171 | 172 | # Run spellcheck 173 | pyspelling -c .spellcheck.yml 174 | ``` 175 | 176 | 6. **Commit and push:** 177 | ```bash 178 | git add . 179 | git commit -m "Add amazing feature" 180 | git push origin feature/amazing-feature 181 | ``` 182 | 183 | 7. **Open a Pull Request** on GitHub 184 | 185 | ### Development Quick Start 186 | 187 | **Run GUI from source:** 188 | ```bash 189 | # Use the PyInstaller entry point 190 | python run_gui.py 191 | 192 | # Or with GUI dependencies installed 193 | pip install -e .[gui] 194 | python run_gui.py 195 | ``` 196 | 197 | **Run CLI from source:** 198 | ```bash 199 | # After pip install -e . 200 | csv-to-qlab path/to/file.csv 127.0.0.1 5 201 | 202 | # Or run as module 203 | python -m app.cli path/to/file.csv 127.0.0.1 5 204 | ``` 205 | 206 | **Run tests:** 207 | ```bash 208 | pytest # All tests 209 | pytest app/tests/test_cli.py # Specific test file 210 | pytest -v # Verbose output 211 | pytest --cov=app # With coverage 212 | ``` 213 | 214 | **Build documentation:** 215 | ```bash 216 | cd website 217 | npm install 218 | npm run build 219 | npm run serve # Preview locally 220 | ``` 221 | 222 | ### Adding New OSC Properties 223 | 224 | Thanks to the configuration-driven architecture, adding new properties is easy - no Python code required! 225 | 226 | 1. Add the property to `app/qlab_osc_config.json` 227 | 2. Add tests to `app/tests/test_osc_config.py` 228 | 3. Update documentation in `website/docs/reference/csv-columns.md` 229 | 230 | See the [Adding Properties Guide](https://fross123.github.io/csv_to_qlab/docs/developer/adding-properties) for detailed instructions. 231 | 232 | ### Building for Distribution 233 | 234 | **macOS Application:** 235 | ```bash 236 | # Install PyInstaller 237 | pip install pyinstaller 238 | 239 | # Build 240 | pyinstaller application.spec 241 | 242 | # Output: dist/csv-to-qlab.app 243 | ``` 244 | 245 | See [Building Releases](https://fross123.github.io/csv_to_qlab/docs/developer/building-releases) for multi-architecture builds and DMG creation. 246 | 247 | ## Project Structure 248 | 249 | ``` 250 | csv_to_qlab/ 251 | ├── app/ 252 | │ ├── application.py # Flask GUI application 253 | │ ├── cli.py # Command-line interface 254 | │ ├── csv_parser.py # Core CSV processing 255 | │ ├── osc_config.py # OSC configuration loader 256 | │ ├── osc_server.py # OSC response handler 257 | │ ├── qlab_osc_config.json # OSC property definitions 258 | │ └── tests/ # Test suite 259 | ├── website/ # Documentation (Docusaurus) 260 | ├── setup.py # Package configuration 261 | ├── requirements.txt # Python dependencies 262 | └── application.spec # PyInstaller config 263 | 264 | ``` 265 | 266 | ## Testing 267 | 268 | The project has comprehensive test coverage across all major components: 269 | 270 | ```bash 271 | # Run all tests 272 | pytest 273 | 274 | # Run specific test categories 275 | pytest app/tests/test_cli.py # CLI tests 276 | pytest app/tests/test_csv_parser.py # CSV parsing tests 277 | pytest app/tests/test_osc_config.py # Configuration tests 278 | 279 | # Run with coverage report 280 | pytest --cov=app --cov-report=html 281 | 282 | # View coverage 283 | open htmlcov/index.html 284 | ``` 285 | 286 | **Test Coverage:** 86% (69 tests) 287 | 288 | ## Troubleshooting 289 | 290 | **GUI won't open on Mac:** 291 | - Right-click the app and select "Open" 292 | - Go to System Preferences → Security & Privacy → Click "Open Anyway" 293 | 294 | **CLI command not found:** 295 | - Ensure pip's bin directory is in your PATH 296 | - Try running: `python -m app.cli` instead 297 | 298 | **Cues not appearing in QLab:** 299 | - Verify QLab is running with a workspace open 300 | - Check the IP address is correct (use `127.0.0.1` for local) 301 | - Ensure firewall isn't blocking port 53000 302 | 303 | **More help:** See [Troubleshooting Guide](https://fross123.github.io/csv_to_qlab/docs/tutorial-basics/send-to-qlab#troubleshooting) 304 | 305 | ## Credits 306 | 307 | Created and maintained by [Finlay Ross](https://github.com/fross123) 308 | 309 | Built with: 310 | - [Python](https://www.python.org/) 311 | - [Flask](https://flask.palletsprojects.com/) 312 | - [PyWebView](https://pywebview.flowrl.com/) 313 | - [python-osc](https://pypi.org/project/python-osc/) 314 | - [Docusaurus](https://docusaurus.io/) 315 | 316 | ## License 317 | 318 | This project is licensed under the GNU General Public License v3.0 - see the [COPYING](COPYING) file for details. 319 | 320 | ## Feedback 321 | 322 | Recommendations for future features are very welcome! Please: 323 | - [Open an issue](https://github.com/fross123/csv_to_qlab/issues/new) for bugs or feature requests 324 | - [Start a discussion](https://github.com/fross123/csv_to_qlab/discussions) for questions or ideas 325 | - Contribute directly with a pull request 326 | 327 | --- 328 | 329 | **Made with ❤️ for the theatre community** 330 | --------------------------------------------------------------------------------