From 73c41303a9dbee9f55a58b21039127fd4b240157 Mon Sep 17 00:00:00 2001 From: Scheianu Ionut Date: Sun, 29 Jun 2025 16:37:59 +0300 Subject: [PATCH] updated logs and players page --- __pycache__/app.cpython-312.pyc | Bin 0 -> 38513 bytes __pycache__/models.cpython-312.pyc | Bin 6171 -> 6257 bytes app.py | 102 ++++++-- instance/dashboard.db | Bin 45056 -> 61440 bytes migrations/README | 1 + migrations/__pycache__/env.cpython-312.pyc | Bin 0 -> 4512 bytes migrations/alembic.ini | 50 ++++ migrations/env.py | 113 +++++++++ migrations/script.py.mako | 24 ++ ...767_add_position_field_to_content_model.py | 46 ++++ ...ion_field_to_content_model.cpython-312.pyc | Bin 0 -> 2074 bytes ...ion_field_to_content_model.cpython-312.pyc | Bin 0 -> 2653 bytes ...472_add_position_field_to_content_model.py | 62 +++++ models.py | 15 +- templates/manage_group.html | 119 +++++++++- templates/player_page.html | 120 +++++++++- .../group_player_management.cpython-312.pyc | Bin 7174 -> 14260 bytes utils/__pycache__/logger.cpython-312.pyc | Bin 4292 -> 5711 bytes utils/__pycache__/uploads.cpython-312.pyc | Bin 16905 -> 16791 bytes utils/group_player_management.py | 218 +++++++++++++++++- utils/logger.py | 21 +- utils/uploads.py | 13 +- 22 files changed, 847 insertions(+), 57 deletions(-) create mode 100644 __pycache__/app.cpython-312.pyc create mode 100644 migrations/README create mode 100644 migrations/__pycache__/env.cpython-312.pyc create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/54d8ece92767_add_position_field_to_content_model.py create mode 100644 migrations/versions/__pycache__/54d8ece92767_add_position_field_to_content_model.cpython-312.pyc create mode 100644 migrations/versions/__pycache__/c2aad6547472_add_position_field_to_content_model.cpython-312.pyc create mode 100644 migrations/versions/c2aad6547472_add_position_field_to_content_model.py diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20adede5ee2c2482e5545d6445189b9b71d59ebe GIT binary patch literal 38513 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

JMS)dQm_hePf(Q^cHuU$QcQVgHaDt~GjpzIB{$ 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 ew1r~iJiMU)*y zaXZ-h+L%fQ-zh04*GM2Ly@n^80jR-pa8}Ub)Z7v=vja2^xgFca`)2r#2}446x?>$orvPjgT;y>XOmTK&XoLAdgjo zD7S&vI!Ft|w0RPR_ga)6VHg%P9sX!nT%DM62gygR9+JLZ5`$&m80m&RUkB-f^ZucG zeVBt)ZJy?)E!$a4TQe3NMw~z_F8>EZ?3Bh9{Ot#^r(d^?%l01K~l3q$5 MIM6/upload', methods=['POST']) @@ -486,6 +489,10 @@ def create_group(): def manage_group(group_id): group = Group.query.get_or_404(group_id) content = get_group_content(group_id) + # Debug content ordering + print("Group content positions before sorting:", [(c.id, c.file_name, c.position) for c in content]) + content = sorted(content, key=lambda c: c.position) + print("Group content positions after sorting:", [(c.id, c.file_name, c.position) for c in content]) return render_template('manage_group.html', group=group, content=content) @app.route('/group//edit', methods=['GET', 'POST']) @@ -516,39 +523,34 @@ def delete_group(group_id): @login_required def group_fullscreen(group_id): group = Group.query.get_or_404(group_id) - content = Content.query.filter(Content.player_id.in_([player.id for player in group.players])).all() + content = Content.query.filter(Content.player_id.in_([player.id for player in group.players])).order_by(Content.position).all() return render_template('group_fullscreen.html', group=group, content=content) @app.route('/group//media//edit', methods=['POST']) @login_required @admin_required -def edit_group_media(group_id, content_id): - group = Group.query.get_or_404(group_id) +def edit_group_media_route(group_id, content_id): new_duration = int(request.form['duration']) + success = edit_group_media(group_id, content_id, new_duration) - # Update the duration for all players in the group - for player in group.players: - content = Content.query.filter_by(player_id=player.id, file_name=Content.query.get(content_id).file_name).first() - if content: - content.duration = new_duration + if success: + flash('Media duration updated successfully.', 'success') + else: + flash('Error updating media duration.', 'danger') - db.session.commit() return redirect(url_for('manage_group', group_id=group_id)) @app.route('/group//media//delete', methods=['POST']) @login_required @admin_required -def delete_group_media(group_id, content_id): - group = Group.query.get_or_404(group_id) - file_name = Content.query.get(content_id).file_name +def delete_group_media_route(group_id, content_id): + success = delete_group_media(group_id, content_id) - # Delete the media for all players in the group - for player in group.players: - content = Content.query.filter_by(player_id=player.id, file_name=file_name).first() - if content: - db.session.delete(content) + if success: + flash('Media deleted successfully.', 'success') + else: + flash('Error deleting media.', 'danger') - db.session.commit() return redirect(url_for('manage_group', group_id=group_id)) @app.route('/api/playlist_version', methods=['GET']) @@ -571,5 +573,55 @@ def get_playlist_version(): 'hashed_quickconnect': player.quickconnect_password }) +@app.route('/player//update_order', methods=['POST']) +@login_required +def update_content_order(player_id): + if not request.is_json: + return jsonify({'success': False, 'error': 'Invalid request format'}), 400 + + player = Player.query.get_or_404(player_id) + if player.groups and current_user.role != 'admin': + return jsonify({'success': False, 'error': 'Cannot reorder playlist for players in groups'}), 403 + + items = request.json.get('items', []) + + success, error, new_version = update_player_content_order(player_id, items) + + if success: + return jsonify({'success': True, 'new_version': new_version}) + else: + return jsonify({'success': False, 'error': error}), 500 + +@app.route('/group//update_order', methods=['POST']) +@login_required +@admin_required +def update_group_content_order_route(group_id): + if not request.is_json: + return jsonify({'success': False, 'error': 'Invalid request format'}), 400 + + items = request.json.get('items', []) + success, error = update_group_content_order(group_id, items) + + if success: + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': error}), 500 + +@app.route('/debug/content_positions/') +@login_required +@admin_required +def debug_content_positions(group_id): + group = Group.query.get_or_404(group_id) + player_ids = [p.id for p in group.players] + + # Query directly with SQL to see positions + sql = text("SELECT id, file_name, position, player_id FROM content WHERE player_id IN :player_ids ORDER BY position") + result = db.session.execute(sql, {"player_ids": tuple(player_ids)}) + + content_data = [{"id": row.id, "file_name": row.file_name, "position": row.position, "player_id": row.player_id} for row in result] + + return jsonify(content_data) + +# Add this at the end of app.py if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0') \ No newline at end of file + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/instance/dashboard.db b/instance/dashboard.db index f9bae3d5dca0d529b319d089e7ad5b1ed007eb50..6abf2db328290b32ac30316a50bc43208d10cf71 100644 GIT binary patch delta 693 zcmZp8z|`=7d4jZ{4FdxMKM->OF%uB;PSi2hv|-RI>*VG7!@$FSo`FA!FOc7mw}DTM zb061Xj?0|&?B{uSxbJgM;-0jz(Vg9-iIJCGTw0p3IkF@%DJL~KKd&S;uLMf*IS08q zhPWz(I6C>bDkwokl_m@DNKf|U7MgsVUvP2`x8CH{{Jg9M`Nf$flfUpPPoBc1U*D+7 z%`UF3&Dg{VHa0OQH8&|UIle5ls5mn}4~@%%Vhx(4f(92SR9k#rX|6(;W013lV~~ci zk*0#5e~5x#sE?110vD&Vzh7`jkfWzxh(cnHF^YBZ1=-080YRR=jzN(M-mZ}f8b}6e za%tM~u!#q1PuAg+P(-sl-VjZIck@0@J4P1%D2B%q8}GAe^RtQDOM-Md6yzjUrWVB; zLTLV`LOynJNlC_5TW|nCL?I+6ig%SDf}lXv0SBlrk06-N=MkEGf?r_rL>_r2Q`5 zNiLSh(A>(MI{EkvQA$9ChtTBXe1el5IrJt=@v2PTz@e<)l+VU4E-A^_Y6&)~ASbahwFpA8 zqZkAc1Dg}UBRKgQui)e=FfBBBE{`CSfx+YrJh~w2K2PvuIex*(dc4*wT$-8f;B8?NVB=F@;9tX^&u`3kpRb!wVY8qD58q}5x%-BUOp5{pHZ}a`2QnTADDi^A dMu35VZw4~t-Hd>n1r6@;PikP>%r@bdJOINVVqyRQ diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..509c236973bf0d625a5b852ec73fd28308621e14 GIT binary patch literal 4512 zcmbVPU2GHC6~5z{u|2lqaS}qv4`K4-5T_wOPy!oRmOw#5yF}GOtCFg+CY~F6j6HVm zj1z*A%B@x^tFF4BMPjS=AyW6DE4C6()xPurUi*?EL6c5Xsa092eG3T{t337G8IPR= z)2@20Ile#V+*Xqx`}a!lSg z?PF-4&9QlInsfSGj?eq2{S3nXY}DJP_CFo)A?(9}Y^*lt^&Y|*v@r;HP`1e(?WPLv z@l1yl{|9I~tOS5YV84#td~RO}!ak9Qm4$~sw*xohvYmCfg*KSXcD=qb9aBPZN~016 z+N4B)Hp9;T?7-{1-f4uM-Q8tO$x&|5bgm!YFsUmqfa%>RF8#M`s>pYq*6*U&t=#oj>kOu{#12l8r~ z$OcXX2)(x*0)=2rR|}eM`;-|wl*UF9YiU)(ws3u+h%XURAa-=pFo-%+GAd)g%knwe zribk$Mlyvw9?@^&(bK0!6g92tn9N}^;+{r#9|2_#7Z(V9Ux!ey)PDe^j5d8df9AQ^ zZi#VIj4!LEIBJO}O!0&zo-@UBtK#{U;Q6mNA}#lSvJ&p!LcTX~PnhDw)8ND=4;(wX z2%GSZG-c!lvyN1AK1St_}&DTpG%HMEP%zmLUPr;#Y+ zu^}t6A?pdw4rF9qh7hv_Lnc&_$_$~(vx6SjesxDES9S3S@Q%WxCxDdEMj-mp!l&O_ z3-oV@hb*zj6nmDxZHfac!GVqN0V{mi3?E+M4m;M{qYZjnM;rfy4rl|Y+p=_aUjw~V zoOKBq)u71MdE-T72%fNqgNpA9_F-jL2!NV=6%jp&S~#ABl(vhr4&r&cLki{S&$ApP z4z3rCA)T3rRC@#Vp)(a+1fqv{f* z638(nf_2HrU^=9kMVeQQjFc_uh9pbhd;dL$U)B^!E5M8nH*pr5Ac&? zffS6ZjY1NQgY7$TWhf1q4-mvtb-7*&QhaZ!JeHV6rhWo?Sc%m#zlatYg*^)=ydITH zzC~ZQnvANKs8%m0(GqJwaqG5vg{k6SWQ|6T!;43S|BY{v`HcA`0}))4uTz?O5>Bkw zIH^1BYaZw8vHrhf|Dz^1_TPvJf!iO}0|f0gJh-AkJU{^JDXhsvEkMY+!AAp&#wP@x z7y3@rP(Xm(TS%$S8jU^;JFe{F$1Xsr#8is9hu3IWG{dv~2uS^eRW~&;iqC^va%Rz| zu+)W?*hRJ;U}vjv4NR5hshf195y->$N(ZWyui2`pJboyUInU_;Y@J_%pRyxz$ta|; z1~6dY%Y>%c+g($jtc3X z3RXOl(foW(x_<1(P7bb1JQM?U14ps`ELl+kO|ubehj8 zz`K~yQ6z;F*7c+(ldOr{p4bm4j#~_$%@tB|PCqyNUu3@zr|4IKK$Z?S-5-DO&b@b* zrygBe3s0=K99d@W{pil+b+N?~yG^m%5(iCja7`RqmwN7oel>QtVIy|%LHJ&HrSt5w z*xQ>tiX8(IIpfx06iLWQAX5pS9jf^i>81;`UQ2|XvY*iJ4?9}L)+7`iy3K-$-6D{F zDp8+w)JEb|nxZf1r{pLQ&?r5}g=befT#=y(hB&CYSD`I+bw(%*y$g^2M<8Xib&%)J zy^Ny9PAk@L#`>2R%-EO}J8#C$m#?e`Vn1K{>C#He@zubImB5+x6H|ADm8KChIQqP) z^OK?SyMGf7SVE^Mbgl|rn<&V~mj@kSZZm^zn}`!a>#f}zodds~dcnhB(~rbMTLI|3 zjG$S(1i?An6RNVoafoUGq; zKhOe|k5$(cj`PP{-X#6lO=@x;zSO{S?>P=nXcg#wWYGfL7Ij)C?#imv@`Oe=IZTOT zR+f8yQh1pULpCgw*hGrCf83+{M4D%Nh{VR%pg${p(IyPy7 z{J87GuCo6{5HaC9<5sxG4EL;|-gTk4j z=%9%XuCyNgylXY_)*6~vkF>g$Sbocl9J3;$W@NN1Y{ginYb(;kbZ-qxOnehcIIg37 b%3|A1wtbE5{4>}7jO+Y6*Y=F-aKht1#8Jiy literal 0 HcmV?d00001 diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/54d8ece92767_add_position_field_to_content_model.py b/migrations/versions/54d8ece92767_add_position_field_to_content_model.py new file mode 100644 index 0000000..bdf6e2d --- /dev/null +++ b/migrations/versions/54d8ece92767_add_position_field_to_content_model.py @@ -0,0 +1,46 @@ +"""Add position field to Content model + +Revision ID: 54d8ece92767 +Revises: +Create Date: 2025-06-29 15:32:30.794390 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '54d8ece92767' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('content', schema=None) as batch_op: + batch_op.add_column(sa.Column('position', sa.Integer(), nullable=True)) + batch_op.alter_column('file_name', + existing_type=sa.VARCHAR(length=120), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.alter_column('player_id', + existing_type=sa.INTEGER(), + nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('content', schema=None) as batch_op: + batch_op.alter_column('player_id', + existing_type=sa.INTEGER(), + nullable=True) + batch_op.alter_column('file_name', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=120), + existing_nullable=False) + batch_op.drop_column('position') + + # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/54d8ece92767_add_position_field_to_content_model.cpython-312.pyc b/migrations/versions/__pycache__/54d8ece92767_add_position_field_to_content_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3bff9e5d43b79e1bf7181b054a64188dc4c7ac0 GIT binary patch literal 2074 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 H%dqTs(UBF~ literal 0 HcmV?d00001 diff --git a/migrations/versions/c2aad6547472_add_position_field_to_content_model.py b/migrations/versions/c2aad6547472_add_position_field_to_content_model.py new file mode 100644 index 0000000..907322d --- /dev/null +++ b/migrations/versions/c2aad6547472_add_position_field_to_content_model.py @@ -0,0 +1,62 @@ +"""Add position field to Content model + +Revision ID: c2aad6547472 +Revises: 54d8ece92767 +Create Date: 2025-06-29 15:58:57.678396 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c2aad6547472' +down_revision = '54d8ece92767' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.alter_column('username', + existing_type=sa.VARCHAR(length=100), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.alter_column('hostname', + existing_type=sa.VARCHAR(length=100), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.alter_column('password', + existing_type=sa.VARCHAR(length=200), + type_=sa.String(length=255), + existing_nullable=False) + batch_op.alter_column('quickconnect_password', + existing_type=sa.VARCHAR(length=200), + type_=sa.String(length=255), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.alter_column('quickconnect_password', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=200), + existing_nullable=True) + batch_op.alter_column('password', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=200), + existing_nullable=False) + batch_op.alter_column('hostname', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=100), + existing_nullable=False) + batch_op.alter_column('username', + existing_type=sa.String(length=255), + type_=sa.VARCHAR(length=100), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/models.py b/models.py index 020ea55..a08713d 100644 --- a/models.py +++ b/models.py @@ -16,17 +16,18 @@ class ServerLog(db.Model): class Content(db.Model): id = db.Column(db.Integer, primary_key=True) - file_name = db.Column(db.String(120), nullable=False) + file_name = db.Column(db.String(255), nullable=False) duration = db.Column(db.Integer, nullable=False) - player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=True) + player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) + position = db.Column(db.Integer, default=0) # This field must exist class Player(db.Model): id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(100), nullable=False, unique=True) - hostname = db.Column(db.String(100), nullable=False) - password = db.Column(db.String(200), nullable=False) - quickconnect_password = db.Column(db.String(200), nullable=True) # Add this field - playlist_version = db.Column(db.Integer, default=0) # Playlist version counter + username = db.Column(db.String(255), nullable=False) + hostname = db.Column(db.String(255), nullable=False) + password = db.Column(db.String(255), nullable=False) + quickconnect_password = db.Column(db.String(255), nullable=True) + playlist_version = db.Column(db.Integer, default=1) # Make sure this exists locked_to_group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=True) locked_to_group = db.relationship('Group', foreign_keys=[locked_to_group_id], backref='locked_players') diff --git a/templates/manage_group.html b/templates/manage_group.html index 35dbc47..6535791 100644 --- a/templates/manage_group.html +++ b/templates/manage_group.html @@ -29,6 +29,26 @@ margin-bottom: 1rem; } } + + .sortable-list li { + cursor: move; + transition: background-color 0.2s ease; + } + + .sortable-list li.dragging { + opacity: 0.5; + background-color: #f8f9fa; + } + + .drag-handle { + cursor: grab; + color: #aaa; + font-size: 1.2rem; + } + + .drag-over { + border-top: 2px solid #0d6efd; + } @@ -71,9 +91,18 @@

{% if content %} -
    +
      {% for media in content %} -
    • +
    • + +
      + + ☰ +
      +

      Media Name: {{ media.file_name }}

      @@ -90,6 +119,8 @@
    • {% endfor %}
    + + {% else %}

    No media uploaded for this group.

    {% endif %} @@ -106,5 +137,89 @@
+ \ No newline at end of file diff --git a/templates/player_page.html b/templates/player_page.html index 77670b0..a4b0a3b 100644 --- a/templates/player_page.html +++ b/templates/player_page.html @@ -29,6 +29,25 @@ margin-bottom: 1rem; } } + .sortable-list li { + cursor: move; + transition: background-color 0.2s ease; + } + + .sortable-list li.dragging { + opacity: 0.5; + background-color: #f8f9fa; + } + + .drag-handle { + cursor: grab; + color: #aaa; + font-size: 1.2rem; + } + + .drag-over { + border-top: 2px solid #0d6efd; + } @@ -74,10 +93,19 @@
{% if content %} -
    +
      {% for media in content %} -
    • +
    • + +
      + + ☰ +
      +

      Media Name: {{ media.file_name }}

      @@ -100,6 +128,8 @@
    • {% endfor %}
    + + {% else %}

    No media uploaded for this player.

    {% endif %} @@ -117,5 +147,91 @@
+ \ No newline at end of file diff --git a/utils/__pycache__/group_player_management.cpython-312.pyc b/utils/__pycache__/group_player_management.cpython-312.pyc index 9a82978dd5d897848b27aaeeb133d0b68222a314..079f22921a7624f5c637f010ec4919bac007514b 100644 GIT binary patch literal 14260 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 VetJ9Ixcmm)Lf1Y%fjK!P_#ZY~>FfXi delta 2538 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 delta 149 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 Nygdk~hr29j{};3og$)1z diff --git a/utils/group_player_management.py b/utils/group_player_management.py index 06e5cac..cf50be9 100644 --- a/utils/group_player_management.py +++ b/utils/group_player_management.py @@ -1,7 +1,12 @@ from models import Player, Group, Content from extensions import db -from utils.logger import log_group_created, log_group_edited, log_group_deleted -from utils.logger import log_player_created, log_player_edited, log_player_deleted +from utils.logger import ( + log_group_created, log_group_edited, log_group_deleted, + log_player_created, log_player_edited, log_player_deleted, + log_player_added_to_group, log_player_removed_from_group, + log_player_unlocked, log_content_reordered, + log_content_duration_changed, log_content_added +) def create_group(name, player_ids): """ @@ -35,6 +40,7 @@ def edit_group(group_id, name, player_ids): Handles locking/unlocking players appropriately. """ group = Group.query.get_or_404(group_id) + old_name = group.name # Store old name in case it changes group.name = name # Get current players in the group @@ -56,6 +62,9 @@ def edit_group(group_id, name, player_ids): # Lock to group player.locked_to_group_id = group.id + + # Log this action + log_player_added_to_group(player.username, name) # Handle players to remove for player_id in players_to_remove: @@ -66,9 +75,19 @@ def edit_group(group_id, name, player_ids): # Unlock from group player.locked_to_group_id = None + + # Log this action + log_player_removed_from_group(player.username, name) + log_player_unlocked(player.username) db.session.commit() - log_group_edited(group.name) + + # Log the group edit + if old_name != name: + log_group_edited(f"{old_name} → {name}") + else: + log_group_edited(name) + return group def delete_group(group_id): @@ -81,6 +100,7 @@ def delete_group(group_id): # Unlock all players in the group for player in group.players: player.locked_to_group_id = None + log_player_unlocked(player.username) db.session.delete(group) db.session.commit() @@ -146,16 +166,190 @@ def delete_player(player_id): def get_group_content(group_id): """ - Get unique content for a group. + Get content for all players in a group, ordered by position. """ + from models import Group, Content + group = Group.query.get_or_404(group_id) - # Get unique media files for the group - content = ( - db.session.query(Content.id, Content.file_name, db.func.min(Content.duration).label('duration')) - .filter(Content.player_id.in_([player.id for player in group.players])) - .group_by(Content.file_name) - .all() - ) + # Get all player IDs in the group + player_ids = [player.id for player in group.players] - return content \ No newline at end of file + # Get unique content based on file_name, preserving position + unique_content = {} + + # For each player, get their content + for player_id in player_ids: + # Get content for this player, ordered by position + player_content = Content.query.filter_by(player_id=player_id).order_by(Content.position).all() + + for content in player_content: + if content.file_name not in unique_content: + unique_content[content.file_name] = content + + # Sort the unique content by position + return sorted(unique_content.values(), key=lambda c: c.position) + +def get_player_content(player_id): + """ + Get content for a specific player, ordered by position. + """ + from models import Content + return Content.query.filter_by(player_id=player_id).order_by(Content.position).all() + +def update_player_content_order(player_id, items): + """ + Update the order of content items for a player. + + Args: + player_id (int): ID of the player + items (list): List of items with id and position + + Returns: + tuple: (success, error_message, new_version) + """ + from models import Player, Content + from extensions import db + + player = Player.query.get_or_404(player_id) + + try: + # Update the position field for each content item + for item in items: + content_id = int(item['id']) + position = int(item['position']) + content = Content.query.get_or_404(content_id) + if content.player_id != player_id: + continue # Skip if not for this player + content.position = position + + # Force increment the playlist version to trigger client refresh + player.playlist_version = (player.playlist_version or 0) + 1 + + db.session.commit() + + # Log the reordering action + log_content_reordered("player", player.username) + + return True, None, player.playlist_version + except Exception as e: + db.session.rollback() + return False, str(e), None + +def update_group_content_order(group_id, items): + """ + Update the order of content items for all players in a group. + + Args: + group_id (int): ID of the group + items (list): List of items with id and position + + Returns: + tuple: (success, error_message) + """ + from models import Group, Content + from extensions import db + + group = Group.query.get_or_404(group_id) + + try: + # Get file names corresponding to the content IDs + content_files = {} + for item in items: + content_id = int(item['id']) + position = int(item['position']) + content = Content.query.get_or_404(content_id) + content_files[content.file_name] = position + + # Update all content items for all players in this group + for player in group.players: + for content in Content.query.filter_by(player_id=player.id).all(): + if content.file_name in content_files: + content.position = content_files[content.file_name] + + # Force increment the playlist version to trigger client refresh + player.playlist_version = (player.playlist_version or 0) + 1 + + db.session.commit() + + # Log the reordering action + log_content_reordered("group", group.name) + + return True, None + except Exception as e: + db.session.rollback() + return False, str(e) + +def edit_group_media(group_id, content_id, new_duration): + """ + Update the duration for all instances of a media item across all players in a group. + + Args: + group_id (int): ID of the group + content_id (int): ID of the content item + new_duration (int): New duration in seconds + + Returns: + bool: Success or failure + """ + from models import Group, Content + from extensions import db + + group = Group.query.get_or_404(group_id) + content = Content.query.get(content_id) + file_name = content.file_name + old_duration = content.duration + + try: + # Update the duration for all players in the group + for player in group.players: + content = Content.query.filter_by(player_id=player.id, file_name=file_name).first() + if content: + content.duration = new_duration + + db.session.commit() + + # Log the duration change + log_content_duration_changed(file_name, old_duration, new_duration, "group", group.name) + + return True + except Exception as e: + db.session.rollback() + return False + +def delete_group_media(group_id, content_id): + """ + Delete a media item from all players in a group. + + Args: + group_id (int): ID of the group + content_id (int): ID of the content item + + Returns: + bool: Success or failure + """ + from models import Group, Content + from extensions import db + + group = Group.query.get_or_404(group_id) + content = Content.query.get(content_id) + file_name = content.file_name + + try: + # Delete the media for all players in the group + count = 0 + for player in group.players: + content = Content.query.filter_by(player_id=player.id, file_name=file_name).first() + if content: + db.session.delete(content) + count += 1 + + db.session.commit() + + # Log the content deletion + log_content_deleted(file_name, "group", group.name) + + return True + except Exception as e: + db.session.rollback() + return False \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py index 3738491..16df638 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -62,4 +62,23 @@ def log_settings_changed(setting_name): log_action(f"Setting '{setting_name}' was changed") def log_files_cleaned(count): - log_action(f"{count} unused files were cleaned from storage") \ No newline at end of file + log_action(f"{count} unused files were cleaned from storage") + +# New logging functions for more detailed activities +def log_player_added_to_group(player_name, group_name): + log_action(f"Player '{player_name}' was added to group '{group_name}'") + +def log_player_removed_from_group(player_name, group_name): + log_action(f"Player '{player_name}' was removed from group '{group_name}'") + +def log_player_unlocked(player_name): + log_action(f"Player '{player_name}' was unlocked from its group") + +def log_content_reordered(target_type, target_name): + log_action(f"Content for {target_type} '{target_name}' was reordered") + +def log_content_duration_changed(content_name, old_duration, new_duration, target_type, target_name): + log_action(f"Duration for '{content_name}' changed from {old_duration}s to {new_duration}s in {target_type} '{target_name}'") + +def log_content_added(content_name, target_type, target_name): + log_action(f"Content '{content_name}' added to {target_type} '{target_name}'") \ No newline at end of file diff --git a/utils/uploads.py b/utils/uploads.py index 7ff9190..f7d27a6 100644 --- a/utils/uploads.py +++ b/utils/uploads.py @@ -5,7 +5,7 @@ from werkzeug.utils import secure_filename from pdf2image import convert_from_path from extensions import db from models import Content, Player, Group -from utils.logger import log_upload, log_process +from utils.logger import log_content_added, log_upload, log_process # Function to add image to playlist def add_image_to_playlist(app, file, filename, duration, target_type, target_id): @@ -24,19 +24,16 @@ def add_image_to_playlist(app, file, filename, duration, target_type, target_id) for player in group.players: new_content = Content(file_name=filename, duration=duration, player_id=player.id) db.session.add(new_content) - player.playlist_version += 1 - group.playlist_version += 1 - # Log the action - log_upload('image', filename, 'group', group.name) + log_content_added(filename, target_type, group.name) elif target_type == 'player': player = Player.query.get_or_404(target_id) new_content = Content(file_name=filename, duration=duration, player_id=target_id) db.session.add(new_content) - player.playlist_version += 1 - # Log the action - log_upload('image', filename, 'player', player.username) + log_content_added(filename, target_type, player.username) db.session.commit() + log_upload('image', filename, target_type, target_id) + return True # Video conversion functions def convert_video(input_file, output_folder):