├── 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 |
({
25 | ...theme,
26 | borderRadius: 0,
27 | colors: {
28 | ...theme.colors,
29 | primary25: 'pink',
30 | primary: 'black',
31 | },
32 | })}
33 | id="dropdown" defaultValue={options[0]}
34 | onChange={handleDropdownChange}
35 | options={options}>
36 |
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 |
53 |
54 | )
55 | } else {
56 | return (
57 |
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 |
35 |
36 |
37 |
There were {{ errors|length }} errors from QLab
38 | we wanted you to know about.
39 |
You also had {{ success|length }} Successfull
40 | commands sent to QLab.
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 |
46 |
47 |
48 |
Congratulations! Your request was successful
49 |
You had {{ successfull_commands|length }} successful
50 | commands sent to QLab.
51 |
Back
52 |
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 |
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 |
35 |
36 |
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 | 
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 | 
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 | 
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 | 
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 | 
89 |
90 | ### 3. Click "Open" one last time
91 |
92 | 
93 |
94 |
95 | ## CSV to QLab should open
96 |
97 | 
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 | 
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 | [](https://github.com/fross123/csv_to_qlab/actions)
4 | [](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 |
--------------------------------------------------------------------------------