initial add of to-print app
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Print queue data (personal)
|
||||
queue.json
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
159
README.md
Normal file
159
README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Print Queue Manager
|
||||
|
||||
A simple command-line tool to store things you want to print when you're away from your home network, then print them all at once when you're back.
|
||||
|
||||
## Features
|
||||
|
||||
- Store file paths and URLs in a print queue
|
||||
- **Watch folder**: Drop files into `~/to-print` and they'll be printed automatically
|
||||
- Print all pending items with a single command
|
||||
- Track what's been printed and what's still pending
|
||||
- Works with your default macOS printer
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- macOS (uses `lp` command for printing)
|
||||
- `curl` (for downloading URLs)
|
||||
|
||||
## Usage
|
||||
|
||||
### Two ways to queue items
|
||||
|
||||
**Method 1: Manually add items to the queue**
|
||||
|
||||
Add a local file:
|
||||
```bash
|
||||
./print_queue.py add /path/to/document.pdf
|
||||
./print_queue.py add ~/Downloads/invoice.pdf --name "Monthly Invoice"
|
||||
```
|
||||
|
||||
Add a URL:
|
||||
```bash
|
||||
./print_queue.py add https://example.com/article.html
|
||||
./print_queue.py add https://example.com/page.html --name "Important Article"
|
||||
```
|
||||
|
||||
**Method 2: Use the watch folder (simpler!)**
|
||||
|
||||
Just drop files into `~/to-print`:
|
||||
```bash
|
||||
cp ~/Downloads/document.pdf ~/to-print/
|
||||
mv ~/Desktop/receipt.pdf ~/to-print/
|
||||
```
|
||||
|
||||
The watch folder accepts: PDF, TXT, JPG, PNG, GIF, DOC, DOCX, HTML, and PS files.
|
||||
|
||||
### List items in the queue
|
||||
|
||||
Show pending items:
|
||||
```bash
|
||||
./print_queue.py list
|
||||
```
|
||||
|
||||
Show all items (including already printed):
|
||||
```bash
|
||||
./print_queue.py list --all
|
||||
```
|
||||
|
||||
### Print all pending items
|
||||
|
||||
Print everything in the queue:
|
||||
```bash
|
||||
./print_queue.py print
|
||||
```
|
||||
|
||||
Preview what would be printed (dry run):
|
||||
```bash
|
||||
./print_queue.py print --dry-run
|
||||
```
|
||||
|
||||
### Remove items from queue
|
||||
|
||||
Remove a specific item by ID:
|
||||
```bash
|
||||
./print_queue.py remove 3
|
||||
```
|
||||
|
||||
Clear all printed items from the queue:
|
||||
```bash
|
||||
./print_queue.py clear
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
**Manual queue:**
|
||||
1. When you're away from your home network, use `add` to store items in the queue
|
||||
2. The queue is stored in `queue.json` in this directory
|
||||
3. When you're back on your home network, run `print` to print everything
|
||||
4. Items are marked as printed but stay in the queue
|
||||
5. Use `clear` to remove printed items when you're done
|
||||
|
||||
**Watch folder:**
|
||||
1. Drop any files into `~/to-print` whenever you want
|
||||
2. When you run `print`, all files in the folder are printed
|
||||
3. After printing, files are moved to `~/to-print/printed/` to avoid reprinting
|
||||
4. You can delete or keep the printed files - they won't be reprinted
|
||||
|
||||
## Examples
|
||||
|
||||
**Using the watch folder (easiest):**
|
||||
```bash
|
||||
# Away from home - just drop files in the folder
|
||||
cp ~/Downloads/receipt.pdf ~/to-print/
|
||||
cp ~/Desktop/ticket.pdf ~/to-print/
|
||||
|
||||
# Back home - check what's ready
|
||||
./print_queue.py list
|
||||
|
||||
# Print everything
|
||||
./print_queue.py print
|
||||
```
|
||||
|
||||
**Using manual queue (for URLs or tracking):**
|
||||
```bash
|
||||
# Away from home - add items to queue
|
||||
./print_queue.py add ~/Downloads/receipt.pdf
|
||||
./print_queue.py add https://github.com/user/repo/blob/main/README.md --name "GitHub README"
|
||||
|
||||
# Back home - check what's queued
|
||||
./print_queue.py list
|
||||
|
||||
# Print everything
|
||||
./print_queue.py print
|
||||
|
||||
# Later, clean up printed items
|
||||
./print_queue.py clear
|
||||
```
|
||||
|
||||
**Mix both methods:**
|
||||
```bash
|
||||
# Add URLs to queue
|
||||
./print_queue.py add https://example.com/article.html
|
||||
|
||||
# Drop files in folder
|
||||
cp ~/Downloads/*.pdf ~/to-print/
|
||||
|
||||
# Print everything at once (queue + folder)
|
||||
./print_queue.py print
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Watch folder is the easiest**: Just drop files into `~/to-print` - no commands needed until you're ready to print
|
||||
- File paths are converted to absolute paths, so they'll work even if you change directories
|
||||
- URLs are downloaded and sent directly to the printer
|
||||
- For better web page printing, consider saving the page as PDF first, then dropping it in the watch folder
|
||||
- Use `--name` to give items friendly names instead of long file paths or URLs
|
||||
- Printed files from the folder are moved to `~/to-print/printed/` - check there if you need to reprint something
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"File not found" error**: Make sure the file path is correct and the file exists
|
||||
|
||||
**Printing fails**:
|
||||
- Check that your printer is set up and `lpstat -p -d` shows your default printer
|
||||
- Make sure you're connected to the network where your printer is available
|
||||
- For URLs, ensure you have internet connectivity
|
||||
|
||||
**Permission denied**: Make sure the script is executable with `chmod +x print_queue.py`
|
||||
296
print_queue.py
Executable file
296
print_queue.py
Executable file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Print Queue Manager - Store items to print when you're not on your home network
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
QUEUE_FILE = "queue.json"
|
||||
WATCH_FOLDER = Path.home() / "to-print"
|
||||
PRINTED_FOLDER = WATCH_FOLDER / "printed"
|
||||
|
||||
|
||||
class PrintQueue:
|
||||
def __init__(self):
|
||||
self.queue_file = Path(__file__).parent / QUEUE_FILE
|
||||
self.queue = self.load_queue()
|
||||
self.watch_folder = WATCH_FOLDER
|
||||
self.printed_folder = PRINTED_FOLDER
|
||||
|
||||
# Create watch folder if it doesn't exist
|
||||
self.watch_folder.mkdir(exist_ok=True)
|
||||
self.printed_folder.mkdir(exist_ok=True)
|
||||
|
||||
def load_queue(self):
|
||||
"""Load the print queue from JSON file"""
|
||||
if self.queue_file.exists():
|
||||
with open(self.queue_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def save_queue(self):
|
||||
"""Save the print queue to JSON file"""
|
||||
with open(self.queue_file, 'w') as f:
|
||||
json.dump(self.queue, f, indent=2)
|
||||
|
||||
def add(self, item, item_type, name=None):
|
||||
"""Add an item to the print queue"""
|
||||
entry = {
|
||||
"id": len(self.queue) + 1,
|
||||
"type": item_type,
|
||||
"item": item,
|
||||
"name": name or item,
|
||||
"added": datetime.now().isoformat(),
|
||||
"printed": False
|
||||
}
|
||||
self.queue.append(entry)
|
||||
self.save_queue()
|
||||
print(f"✓ Added to queue: {entry['name']}")
|
||||
return entry
|
||||
|
||||
def list_items(self, show_all=False):
|
||||
"""List items in the queue"""
|
||||
items = self.queue if show_all else [i for i in self.queue if not i['printed']]
|
||||
folder_files = self.get_folder_files()
|
||||
|
||||
if not items and not folder_files:
|
||||
print("Nothing to print!")
|
||||
return
|
||||
|
||||
if items:
|
||||
print("\nManually Added Queue:")
|
||||
print("-" * 60)
|
||||
for item in items:
|
||||
status = "✓ PRINTED" if item['printed'] else "⏳ PENDING"
|
||||
print(f"[{item['id']}] {status}")
|
||||
print(f" Name: {item['name']}")
|
||||
print(f" Type: {item['type']}")
|
||||
print(f" Added: {item['added'][:10]}")
|
||||
if item['printed']:
|
||||
print(f" Printed: {item.get('printed_at', 'N/A')[:10]}")
|
||||
print()
|
||||
|
||||
if folder_files:
|
||||
print(f"\nFiles in Watch Folder ({self.watch_folder}):")
|
||||
print("-" * 60)
|
||||
for file in folder_files:
|
||||
print(f"⏳ {file.name}")
|
||||
print(f"\nTotal: {len(folder_files)} file(s)")
|
||||
print()
|
||||
|
||||
def print_all(self, dry_run=False):
|
||||
"""Print all pending items in the queue and folder"""
|
||||
pending = [i for i in self.queue if not i['printed']]
|
||||
folder_files = self.get_folder_files()
|
||||
|
||||
total = len(pending) + len(folder_files)
|
||||
|
||||
if total == 0:
|
||||
print("Nothing to print!")
|
||||
return
|
||||
|
||||
print(f"\n{'Would print' if dry_run else 'Printing'} {total} item(s)...\n")
|
||||
|
||||
# Print queue items
|
||||
if pending:
|
||||
print("From manually added queue:")
|
||||
for item in pending:
|
||||
try:
|
||||
if dry_run:
|
||||
print(f" Would print: {item['name']} ({item['type']})")
|
||||
else:
|
||||
print(f" Printing: {item['name']}...")
|
||||
self.print_item(item)
|
||||
item['printed'] = True
|
||||
item['printed_at'] = datetime.now().isoformat()
|
||||
print(f" ✓ Success")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
if not dry_run:
|
||||
self.save_queue()
|
||||
print()
|
||||
|
||||
# Print folder files
|
||||
if folder_files:
|
||||
print(f"From watch folder ({self.watch_folder}):")
|
||||
for file in folder_files:
|
||||
try:
|
||||
if dry_run:
|
||||
print(f" Would print: {file.name}")
|
||||
else:
|
||||
print(f" Printing: {file.name}...")
|
||||
self.print_folder_file(file)
|
||||
print(f" ✓ Success (moved to {self.printed_folder.name}/)")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
print()
|
||||
|
||||
print("Done! Use 'list --all' to see all items or 'clear' to remove printed items.")
|
||||
|
||||
def print_item(self, item):
|
||||
"""Print a single item"""
|
||||
if item['type'] == 'file':
|
||||
# Expand user home directory and make path absolute
|
||||
file_path = Path(item['item']).expanduser().resolve()
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
subprocess.run(['lp', str(file_path)], check=True, capture_output=True)
|
||||
|
||||
elif item['type'] == 'url':
|
||||
# Download URL and print
|
||||
# For web pages, you might want to use wkhtmltopdf for better formatting
|
||||
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as tmp:
|
||||
try:
|
||||
print(f" Downloading...")
|
||||
result = subprocess.run(
|
||||
['curl', '-L', '-s', item['item'], '-o', tmp.name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Check if file has content
|
||||
if os.path.getsize(tmp.name) == 0:
|
||||
raise ValueError("Downloaded file is empty")
|
||||
|
||||
print(f" Sending to printer...")
|
||||
subprocess.run(['lp', tmp.name], check=True, capture_output=True)
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(tmp.name):
|
||||
os.unlink(tmp.name)
|
||||
|
||||
def clear_printed(self):
|
||||
"""Remove printed items from the queue"""
|
||||
original_count = len(self.queue)
|
||||
self.queue = [i for i in self.queue if not i['printed']]
|
||||
removed = original_count - len(self.queue)
|
||||
self.save_queue()
|
||||
print(f"✓ Removed {removed} printed item(s) from queue")
|
||||
|
||||
def remove_item(self, item_id):
|
||||
"""Remove a specific item from the queue by ID"""
|
||||
original_queue = self.queue.copy()
|
||||
self.queue = [i for i in self.queue if i['id'] != item_id]
|
||||
|
||||
if len(self.queue) == len(original_queue):
|
||||
print(f"✗ Item {item_id} not found")
|
||||
return False
|
||||
|
||||
self.save_queue()
|
||||
print(f"✓ Removed item {item_id} from queue")
|
||||
return True
|
||||
|
||||
def get_folder_files(self):
|
||||
"""Get all printable files from the watch folder"""
|
||||
# Common printable file extensions
|
||||
printable_extensions = {'.pdf', '.txt', '.jpg', '.jpeg', '.png', '.gif',
|
||||
'.doc', '.docx', '.html', '.htm', '.ps'}
|
||||
|
||||
files = []
|
||||
if self.watch_folder.exists():
|
||||
for file in self.watch_folder.iterdir():
|
||||
if file.is_file() and file.suffix.lower() in printable_extensions:
|
||||
files.append(file)
|
||||
|
||||
return sorted(files, key=lambda f: f.stat().st_mtime)
|
||||
|
||||
def print_folder_file(self, file_path):
|
||||
"""Print a file from the watch folder and move it to printed folder"""
|
||||
subprocess.run(['lp', str(file_path)], check=True, capture_output=True)
|
||||
|
||||
# Move to printed folder
|
||||
dest = self.printed_folder / file_path.name
|
||||
# Handle name conflicts
|
||||
counter = 1
|
||||
while dest.exists():
|
||||
stem = file_path.stem
|
||||
suffix = file_path.suffix
|
||||
dest = self.printed_folder / f"{stem}_{counter}{suffix}"
|
||||
counter += 1
|
||||
|
||||
file_path.rename(dest)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Print Queue Manager - Store and print files when on your home network',
|
||||
epilog='Examples:\n'
|
||||
' %(prog)s add document.pdf\n'
|
||||
' %(prog)s add https://example.com/page.html --name "Example Page"\n'
|
||||
' %(prog)s list\n'
|
||||
' %(prog)s print\n',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# Add command
|
||||
add_parser = subparsers.add_parser('add', help='Add item to print queue')
|
||||
add_parser.add_argument('item', help='File path or URL to print')
|
||||
add_parser.add_argument('--name', help='Custom name for the item')
|
||||
|
||||
# List command
|
||||
list_parser = subparsers.add_parser('list', help='List items in queue')
|
||||
list_parser.add_argument('--all', action='store_true',
|
||||
help='Show all items including printed ones')
|
||||
|
||||
# Print command
|
||||
print_parser = subparsers.add_parser('print', help='Print all pending items')
|
||||
print_parser.add_argument('--dry-run', action='store_true',
|
||||
help='Show what would be printed without actually printing')
|
||||
|
||||
# Clear command
|
||||
clear_parser = subparsers.add_parser('clear',
|
||||
help='Remove printed items from queue')
|
||||
|
||||
# Remove command
|
||||
remove_parser = subparsers.add_parser('remove',
|
||||
help='Remove a specific item by ID')
|
||||
remove_parser.add_argument('id', type=int, help='Item ID to remove')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
queue = PrintQueue()
|
||||
|
||||
if args.command == 'add':
|
||||
# Detect if it's a URL or file
|
||||
item_type = 'url' if args.item.startswith(('http://', 'https://')) else 'file'
|
||||
|
||||
# Validate file exists if it's a file path
|
||||
if item_type == 'file':
|
||||
file_path = Path(args.item).expanduser().resolve()
|
||||
if not file_path.exists():
|
||||
print(f"✗ Error: File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
# Store the absolute path
|
||||
args.item = str(file_path)
|
||||
|
||||
queue.add(args.item, item_type, args.name)
|
||||
|
||||
elif args.command == 'list':
|
||||
queue.list_items(show_all=args.all)
|
||||
|
||||
elif args.command == 'print':
|
||||
queue.print_all(dry_run=args.dry_run)
|
||||
|
||||
elif args.command == 'clear':
|
||||
queue.clear_printed()
|
||||
|
||||
elif args.command == 'remove':
|
||||
queue.remove_item(args.id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# No external dependencies required
|
||||
# This project uses only Python 3 standard library
|
||||
# System requirements:
|
||||
# - Python 3.6+
|
||||
# - curl (for downloading URLs)
|
||||
# - lp (macOS/Linux printing command)
|
||||
Reference in New Issue
Block a user