diff --git a/fhem/CHANGED b/fhem/CHANGED index ad8d8856b..e9115141b 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,1496 +1,1497 @@ -# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. -# Do not insert empty lines here, update check depends on it. +# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. +# Do not insert empty lines here, update check depends on it. + - added: SONOS and SONOSPLAYER to support Sonos Multiroom Audiosystems (Reinerlein) - change: 64_ESA2000.pm: add batterystate - added: 42_SMARTMON: Frontend to smartctl (maintainer: hexenmeister) - - feature: 70_PushNotifier added line break in Messages (xusader) - - feature: readingsGroup: added valuePrefix and valueSuffix attributes - added collapsed/collapsible to visibility attribute - added visibility command - - bugfix: FB_CALLMONITOR: fixing not working company numbers - reverse search for search.ch - - bugfix: 70_PushNotifier repair set function (xusader) - - bugfix: PRESENCE: fixing not working timer, when using set [...] statusRequest - - bugfix: FB_CALLMONITOR: fixing reverse search for klicktel.de - - feature: new module 52_I2C_MCP342x.pm added (klausw) - - feature: SYSMON: read cpu temp on FritzBox - - feature: ios7smallscreenstyle.css: table width based on screen width, new - header, links colored in detail view - - feature: new module FRITZBOX: controls Fritz!Box router and Fritz!Fon - - feature: new module 52_I2C_EEPROM.pm added (klausw) - - feature: readingsGroup: added ! flag, - added visibility and cellStyle attributes - - feature: new module 52_I2C_MCP23008.pm added (klausw) - - feature: new module 98_logProxy.pm added (justme1968) - - change: 66_ECMD: ReadyFn added (fixes issue under Windows) - - change: 02_RSS: use a GUID in RSS; urlq source for img command - - feature: 70_PushNotifier improve usebility, configuration without cURL (xusader) - - bugfix: SYSMON: prevent empty line im log by userReadings - - feature: 10_IT empfang (by bjoernh) - - bugfix: PRESENCE: fix race condition, when delete disabled attribute and - PRESENCE does not start to scan. - - feature: OPENWEATHER: captures weather forecast from API of www.wetter.com - - fhem 5.6 released - -- 2014-11-09 (5.6) - - bugfix: FB_CALLMONITOR: fixing race condition of missing events while - performing multiple calls - - feature: PROPLANTA: captures weather forecast from web page www.proplanta.de - - feature: 15_CUL_EM added attribute maxPeak (arnoaugustin) - - bugfix: 10_IT changed "setstate" to avoid eventMap errors (arnoaugustin) - - feature: new module 37_harmony.pm added (justme1968) - - change: WMBUS: use _ instead of : as readings separator, better support for EnergyCam - - feature: new module 23_KOSTALPIKO added (john) - - feature: new module 98_HourCounter added, 99_UtilsHourCounter.pm added to contrib (john) - - added: MYSENSORS: connect to serial or Ethernet MySensors Gateway - - added: MYSENSORS_DEVICE: represent a MySensors sensor- or actor node - - feature: global ATTR/DELETEATTR/MODIFIED events - - feature: 55_GDS.pm - attr disable added - - bugfix: SYSMON: prevent endless loop at startup with 'disable' attribute - - feature: SYSMON: added FritzBox informations: DSL rate, DSLAM sync time, count of CRC an FEC - - bugfix: SYSMON: unwanted characters in dsl info lines - - change: 57_Calendar: process continuation lines, get/set syntax checks - - bugfix: SYSMON: fix availability of cpu/kernel_max - - bugfix: SYSMON: numeric check - - change: 59_Weather: change icons for conditions 31, 34, 36 - - added: MQTT: connect fhem with mqtt - - added: MQTT_BRIDGE: bidirectional mapping of existing fhem-device to mqtt-topic - - added: MQTT_DEVICE: fhem-device that can be controlled by and publishes to mqtt - - added: I2C_LCD: module to drive PCF8574T based LCD connected via I2C - - added: I2C_DS1307: module to read time and date from DS1307 connected by i2c - - added: OWX_ASYNC: asynchronous, non-blocking version of OWX for DS2480, DS9097 and FRM - - feature FRM: work as physical IODev for I2C_XXX modules - added: FRM_ROTENC: read rotary-encoders with FRM - added: FRM_RGB: control rgb-leds with FRM - added: FRM_STEPPER: control stepper-motors with FRM - added: FRM_AD: analog input for FRM - added: FRM_PWM: analog (pwm) output for FRM - added: FRM_SERVO: control servo-motors with FRM - - added: FRM_IN: digital input for FRM - - added: FRM_OUT: digital output for FRM - - added: FRM: connect Arduino with firmata to fhem - - change: 57_Calendar: line parsing rewritten, care for missing - modification timestamps - - change: SYSMON: support userReadings in SYSMON_ShowValues - - change: 59_Weather: change icon for condition clear to sunny.png - - bugfix: 57_Calendar: calendar event anymore in modeAlarmed if started - - feature: 57_Calendar: deal with non-existent end times - - bugfix: SOMFY: fix non-working on/off-for-timer methods - made positioning attributes optional - - feature: SOMFY: support for exact positioning (one-time setup of run times required) - support for parse()-function, requires newest CULFW. - - feature: userattr is now also device attribute - - feature: ZWave: Fibaro_FGRM222 MANUFACTURER_PROPRIETARY class - - feature: sequence: reportEvents attribtue added - - feature: SYSMON: RAM and SWAP Readings on OSX - - change: 34_NUT: removed calculation of values. Use userReadings instead. - removed autogeneration of attr model and serNo. - - feature: SYSMON: improvement: support network information (IP, IPv6) on german linux - - feature: Synology DiskStation NAS basic spk file creation - - change: 34_NUT: readingFnAttributes added; creation of units deleted; - changed attr asReadings to use comma instead of space - - bugfix: SYSMON: crash on FritzBox - - bugfix: 34_NUT: fixed possible buffer overflow, rewrote reception of data - - bugfix: SYSMON: idletime on multicore, warnings - - change: 09_CUL_FHTTK.pm: modified set option for sync, open and closed - - feature: SYSMON: HTML/Text output for SYSMON-CloneDummies - - feature: SYSMON: Method for titled HTML/Text output - - added: 34_NUT.pm (maintainer: creideiki) - - feature: SYSMON: added new reading: perl_version - - feature: add toggle to SetExtensions (introduced for ZWave) - - bugfix: plotEmbed FHEMWEB attribute (fix for an iOS8 bug) - - feature: SHC: support for analog inputs (EnvSensor) and new device - RGB_Dimmer added (rr2000) - - feature: PRESENCE: MAC address support for mode fritzbox (by Markus M.) - - bugfix: PRESENCE: fixing presence detection in mode fritzbox with new - Fritz!OS 6.20 (by Markus M.) - - feature: FB_CALLMONITOR: reverse-search attribute is now providing all - possible values, which are selectable (via fhemweb_multiple.js). - see commandref for all possible values - - feature: speed up through caching of postproc and regex in ECMDDevice - - bugfix: fixed handling of autocreation for 10_OWServer.pm - - feature: option to cope with partial messages in ECMD/ECMDDevice - - bugfix: SOMFY: add module to CUL client list, to set IODev automatically - - feature: sequence: triggerPartial Attribute added - - feature: 36_JeeLink: changed flash command to use fhem firmware - directory (by HCS) - - feature: 70_ENIGMA2: new attribute lightMode for old/slow devices - limited restricted functionality - - added: 98_CustomReadings.pm (maintainer: HCS) - - change: 98_Text2Speech.pm: fix a problem with microseconds in time() - by using mp3-templates or playing mp3 directly - - feature: state definition and split attribute added to 66_ECMD, - 67_ECMDDevice - - FHEMWEB: JavaScripts and CssFiles attributes added - - change: avoid updating weather information on get (59_Weather.pm) - - change: removed noshutdown=0 for HTTP connections made in 57_Calendar.pm - and 59_Weather.pm to address issues when FHEM is behind - a web proxy - - feature: update rewritten, restore added - - feature: enabled JavaScript in 02_RSS to support WebViewControl - - added: new module 36_WMBUS.pm (kaihs) Wireless M-Bus - - feature: SYSMON: aded new plots (power infos for cubietruck) - - feature: SYSMON: aded new readings for each network interface: ip and ip6 - - feature: SYSMON: aded power supply informations to the text output method - - feature: SYSMON: power supply informations (ac, usb, battery) - - feature: added 70_PushNotifier.pm - - feature: 70_VIERA: Add parameter "HDMI1" - "HDMI4" for command remoteControl - to select HDMI input directly. - Add command "input" to select a HDMI port, TV or SD-Card - as source - - bugfix: LevelSender: Version 1.0.5: Could not get compiled by the Arduino - IDE - - feature: PRESENCE: new event "error" and "timeout" for state reading to - indicate a non successful check - - bugfix: 70_Jabber: fixed UTF8 encoding/decoding of messages - - feature: 10_OWServer autocreate coexists with OWXXX modules (Boris & - ntruchsess) - - feature: added 36_Level.pm - - feature: netatmo: added plz support for public stations - - change: 70_ENIGMA2: keep reading for recordings up-to-date during standby - - bugfix: 98_Text2Speech: - playing mp3files directly, eg: :ring.mp3: - - playing any mp3file only text - - feature: FB_CALLMONITOR: new reading "direction" to differentiate - between incoming and outgoing call. - - feature: FB_CALLMONITOR: all informational readings about a call will be - triggered for each call event - (call, ring, connect and disconnect) - - feature: mailcheck: allow user and pssword as perl expression - - feature: netatmo: support for public stations - - feature: PRESENCE: new set command "power" to execute a FHEM command - which can power on or off the checked device (given via attribute) - - feature: readingsGroup: added valueColumn attribute - - feature: readingsGroup: added ...,@,... argument format - - feature: 52_I2C_PCF8574.pm: added attribute OnStartup - 52_I2C_PCA9532.pm: added attribute OnStartup, - added attribute OutputPorts as substitute for InputPorts - 51_RPI_GPIO.pm: changed access to gpio via userspace by default - (for BBB and Cubie), access via gpio utility as fallback - - feature: PIONEERAVR: new attribute: checkConnection - - change: do no parse empty lines in 57_Calendar.pm - - bugfix: 10_SOMFY.pm: save enc-key and rolling-code as reading instead of - attribute to prevent loss of control after FHEM restart. - - feature: new module 10_SOMFY.pm to support Somfy RTS blinds - - bugfix: 70_PIONEERAVR.pm: fix for STATE if connection is lost - - bugfix: 37_SHC.pm: Move xml file under subdir lib, otherwise it won't be - deployed during update - - bugfix: 70_PIONEERAVR.pm: player commands are now available for more inputs - "play" was not in the drop down list of available set commands - check every 120s if the data connection to the Pioneer AV - receiver is still up - check if we get a reply from the Pioneer AV receiver not later - than 3s after a command was sent - fix for alias names of inputs - more input presets (spotify, mhl, hdmi7, hdmi8), inputs are now - queried from 1 - 59 - 71_PIONEERAVRZONE.pm: bugfix:logging, set input - - change: 00_RPII2C.pm: hardware access changed to ioctl and syswrite/read - SMBus module not needed anymore but still usable, see attribute - useHWLib possibility to swap I2C-0 to P5 for Rev. B raspberries - via attribute swap_i2c0 (not tested yet) - - feature: 70_ENIGMA2: add attribute - remotecontrol=[standard,advanced,keyboard] - - bugfix: 70_PIONEERAVR.pm and PIONEERAVRZONE.pm: added "use SetExtensions", - commandref updates fixed RC_layout - - feature: new modules 70_PIONEERAVR.pm and PIONEERAVRZONE.pm - - feature: FLOORPLAN has new style 7 to display commands only. - - added: 89_HEATRONIC.pm (heikoranft) - - bugfix: duration parsing of calendar events in 57_Calendar.pm fixed - - feature: LightScene: added followDevices attribute - - feature: non-blocking retrieval of data in 59_Weather.pm (Boris & herrmannj) - - feature: new modules 37_SHC.pm and 37_SHCdev.pm added (rr2000) - - feature: 36_EMT7110: added this new module - - feature: 36_JeeLink: added initCommands attribute and flash command (by HCS) - - feature: SYSMON: DECT Temperatur - - bugfix: SYSMON: prevent some warnings - - change: SYSMONremoved support for old format of filesystem reading - - change: moved 98_openweathermap.pm to contrib - moved 98_geodata.pm to contrib - moved 55_BBB_BMP180.pm to contrib - - change: honor DURATION in 57_Calendar.pm - - bugfix: YAMAHA_AVR: don't let FHEM hang anymore, when the receiver - is not reachable - - change: 55_GDS.pm: use Blocking.pm for retrieval of large files - - change: YAMAHA_BD: make YAMAHA_BD more performant by using non-blocking - HTTP request (from HttpUtils.pm) - - added: YAMAHA_BD: new set command trickPlay and more remoteControl - commands. new reading trickPlay - - added: new module 98_statistics.pm: hourly, daily, monthly, yearly - statistics for min/avg/max/delta/duration of selected readings - - added: new module 52_I2C_MCP23017.pm (klausw) - - feature: Dashboard Configuration-Dialog for Tabs - - feature: new module 33_readingsHistory.pm added (justme1968) - - feature: new command copy (justme1968) - - feature: enabled GIF, PNG and JPG as background image formats, enabled - relative font size changed and perl specials for font size - in 02_RSS.pm - - feature: YAMAHA_AVR: new set commands and readings for controlling - the sound output behavior (Enhancer, DSP and straight - output) and sleep timer. For details, see commandref. - - bugfix: configdb filemove not working after previous changes - - change: IMPORTANT CHANGES TO configDB! - changed: all files will be imported as binary - changed: all existing textfiles will be moved to binary - removed: command binfileimport - added: sorted write and read of configuration data - - bugfix: SYSMON: css class name (sysmon) - - feature: option to determine the number of icons from WeatherAsHtml - - feature: DbLog: (thanks to betateilchen) - * added new global modules function $hash->{DbLog_splitFn} - to let split the generated events by the own module - into readingsname, value and unit - * added SVG_sampleDataFn - * added FW_detailFn - - added: new module contrib/97_SprinkleControl.pm (tobiasfaust) - - added: new module contrib/98_Sprinkle.pm (tobiasfaust) - both modules helps to control the sprinkles in your garden - --> take a look to the Wiki-Article - http://www.fhemwiki.de/wiki/Bew%C3%A4sserungssteuerung - - feature: FB_CALLMONITOR: new reading "missed_call_line" indicating - the line number which received the missed call - - feature: YAMAHA_AVR current* readings will be erased in case they - not applicable - - feature: YAMAHA_AVR currentTitle available for TUNER - - feature: new Method: SYSMON_getValues([desired keys]) - - feature: JSONMETER: hourly statistics - - feature: configdb: new command search - - feature: LUXTRONIK2: estimation of electrical power consumption, - considers time depending tariffs (activeTariff) - - added: configDB functions for handling binary files - - feature: 02_RSS.pm: alpha channel for colors - - feature: JSONMETER: time depending tariffs added (activeTariff) - - updated: codemirror version 3.24 - - feature: new module 35_SWAP_0000002200000008 for panstamp - indoor multi sensor board with tft - - added: new module 36_EC3000.pm (justme1968) - - feature: IT: added support for set-extensions (justme1968) - - added: new modules 10_Itach_IR and 88_Itach_IRDevice to - use Itach WF2IR or IP2IR to be used as universal - infrared remotecontrol - - added: new module 51_I2C_TSL2561.pm (kaihs) - - added: new module 02_FRAMEBUFFER.pm (kaihs) - - feature: SYSMON: many FritzBox specific readings: - wlan_state, wlan_guest_state, internet_ip, internet_state, - night_time_ctrl, num_new_messages, fw_version_info - - feature: configDB: added command fileshow - - feature: configDB: added commands filelist and filedelete - - feature: configDB: added commands fileimport and fileexport - - feature: 36_JeeLink: added LaCrosse, ETH200comfort, CUL_IR, - HX2272 and FS20 modes from ulli - added AliRF - added Clients and MatchList attribute - - feature: 02_RSS.pm: HTTPS enabled, png as image type added, autofreshing - HTML page with image map added - - feature: ECMD and ECMDDevice completely reworked, see - http://forum.fhem.de/index.php/topic,21515.0.html - - feature: new layout command rect and new attribute bgcolor for RSS - - added: new module 55_weco.pm (betateilchen) - - added: new module 70_Jabber.pm (BioS) - - bugfix: Dashboard: use "loadScript" for load JavaScripts - - feature: new module 00_NetzerI2C.pm, 51_Netzer.pm added (klausw) - - feature: new command reload for 57_Calendar.pm forces cleanup - - changed: small update to the documentation of recurring events in - 57_Calendar.pm - - bugfix: PRESENCE: fixing wrong presence state for mode lan-ping - when device is unreachable - - feature: 10_EnOcean: new EEP profiles: D2-01-00 - D2-01-11 (VLD) - - changed: 00_TCM/10_EnOcean: learning mode (teach-in / teach-out) changed - and extended - - added: new module 10_UNIRoll.pm (c-herrmann) - - feature: cloneDummy: new attribut cloneIgnore - - feature: cloneDummy: new optional parameter [reading] - - bugfix: Dashboard: dashboard_showfullsize not applied in room "all" - - feature: new module 98_PID20.pm added (John / betateilchen) - - feature: new module 00_RPII2C.pm, 52_I2C_PCA9532.pm, 52_I2C_PCF8574.pm, - 52_I2C_SHT21.pm added (klausw) - - change: module 71_LISTENLIVE.pm moved to contrib - module 23_WEBTHERM.pm moved to contrib - - change: module 98_PID.pm moved to contrib as preparation for - next major replace. Replaced by 98_PID20.pm (John/betateilchen) - - change: openweathermap: added set command "clear" - - change: MAX: interpret SetTemperature command from WT to HT - - feature: MAX: retry packets 3 times if missing an ack - - feature: new module 98_cloneDummy.pm added (Joachim) - - feature: STACKABLE_CC (busware.de device for the RPi) added - - feature: configdb export/import added for data security (betateilchen) - - feature: new module 38_netatmo.pm added (justme1968) - - change: 09_CUL_FHTTK.pm: clean up code to avoid "Use of uninitialized - value in concatenation.." - - change: 09_CUL_FHTTK.pm: extend module list to FHT80TF and FHT80TF-2 - and update of documentation (matscher) - - feature: disabledForIntervals attribute added for at/notify/watchdog - - feature: jsonlist2 added, jsonlist is deprecated. - - feature: DbLog: Added new function : ReadingsVal/ReadingsTimestamp - - feature: Text2Speech: added new attribute TTS_VolumeAdjust - - feature: new module 70_PHTV.pm (loredo) - - feature: JSONMETER: added statistic functions - - feature: LightScene: added scene editor from UliM - - feature: SYSMON: New method: SYSMON_ShowValuesText - - feature: configDB.pm use sql database instead of fhem.cfg (betateilchen) - - feature: new module 98_geodata collect location based data (betateilchen) - - feature: 98_pilight: Added support for Elso protocol - - feature: readingsGroup: added sortDevices attribute - - feature: ENIGMA2: new reading 'recordings', new command record - - change: ENIGMA2: rewrite for NonBlocking - - feature: SYSMOM: new Plot + Doc - - feature: Dashboard: Custom CSS Attribute. Max. 7 Tabs. - - bugfix: Dashboard: Change Groupcontent sorting. Fix Bug that affect - new Groups. - - feature: 10_EnOcean: UTE protocol implemented - - feature: 00_TCM: new command teach - - bugfix: SYSMOM: uninitialized value warning on FritzBox - - added: 09_CUL_FHTTK.pm: german module documentation (matscher) - - feature: readingsGroup: allow FHEMWEB slider and dropdown menus as commands - - feature FB_CALLMONITOR: new attribute "disable" to - disable FB_CALLMONITOR - - feature: YAMAHA_BD: new attribute "disable" to disable cyclic status - updates of player - - change: 09_CUL_FHTTK.pm: added event-on...readings and event-min-interval - updated to reading update mechanism (matscher) - - feature: Dashboard: Groupstitel now can show icons, - Backbutton in Fullsize-Mode - - deleted: 51_BBB_WATCHDOG.pm - not really needed - - bugfix: DbLog: adding ShutdownFunction - - feature: YAMAHA_AVR: new attribute "disable" to disable cyclic status - updates of receiver - - feature: LightScene: added attribute switchingOrder - - added: new module 00_THZ.pm (immiimmi) - - added: new module 98_HTTPMOD.pm (stefanstrobel) - - added: new module 51_BBB_WATCHDOG.pm (betateilchen) - - bugfix: SYSMON: Fix: uninitialized variable - - feature: new modul 73_MPD added (Wzut) - - bugfix: SYSMON: Fix: root fs with /dev/mapper - - feature: Dashboard: The display of the dashboard can be limited to a - defined FHEMWEB. Change view of readingroups - - feature: new module 70_JSONMETER to read obis compatible data in json - format from so called smart meters for electricity, gas or heat - - feature: new modules 10_RESIDENTS, 20_ROOMMATE and 20_GUEST added (loredo) - - feature: LUXTRONIK2: attribute 'doStatistics' calculates boiler gradients - - feature: GEOFANCY: support both apps, Geofency.app and Geofancy.app - - feature: LightScene: added attribute lightSceneRestoreOnlyIfChanged - - bugfix: SYSMON: Fix: CPUTemp & BogoMIPS for utilite-Box. - - bugfix: PRESENCE: fix present-check-interval to be equal with normal - check-interval if not set in define statement and not 30 sec. - - feature: DASHBOARD: Tabs can show an icon. - - bugfix: DASHBOARD: dashboard_showfullsize only in DashboardRoom. - Fix showhelper Bug on lock/unlock. The error that after a trigger - action the curren tab is changed to the "old" - activetab tab has been fixed. - - bugfix: SYSMON: Filesystems (absent medium) - - feature: FLOORPLAN-menu-items can get icons attached by new - attribute fp_roomIcons - - feature: FLOORPLAN-specific icons can now be assigned by just - creating a folder under fhem/images with the flooplan-name - - feature: DASHBOARD: Tabs can set on top, bottom or hidden. - - bugfix: SYSMON: another format for ifconfig output - - feature: DASHBOARD: Use longpoll to update content. - rowcentercolwidth can now be defined per column. - Dashboard can hide FHEMWEB Roomliste and Header => - Fullsizemode. - - bugfix: SYSMON: null reading for absent mount points - - feature: DbLog: jokers "%" in device/reading definition are now possible - - feature: SYSMON: new CPU Statistics and Plots - - feature: changed 10_OWServer.pm and 11_OWDevice.pm to use - NOTIFYDEV (justme1968) - - feature: LightScene: added setcmd command - - feature: DASHBOARD: Dashboard get Tabs. Redesign saving of Group - positioning. - - bugfix: SYSMON: Log Warnings, unnoetige Readings erkenen und entfernen - - feature: LUXTRONIK2: Setting of controller parameter and internal clock - - feature: new module 71_YAMAHA_BD.pm to control Yamaha Blu-Ray - players over network. - - bugfix: DbLog: fix for plotfork - - bugfix: SYSMON: filesystems may be wrong on some systems - - feature: new module 98_pilight.pm added (andreas-fey) - - change: LUXTRONIK2 - made compatible with current developer guidelines - (Blocking.pm, reading update mechanism) - - feature: readingsGroup: added icons and links/commands - - feature: new module 98_Text2Speech.pm added (Tobias Faust) - Google Translator Engine or ESpeak can be used - - feature: YAMAHA_AVR: define separate on and off status intervals for - cyclic status updates - - feature: Visualizations (Plots) for SYSMON added - - feature: new module 42_SYSMON.pm added (hexenmeister) - - feature: YAMAHA_AVR: new readings for radio stations, current title - and more. see commandref for more details. - - feature: new module 32_withings.pm added (justme1968) - - bugfix: PRESENCE: fixing user detection on FritzBox! - - feature: new module 38_CO20.pm added (justme1968) - - feature: new module 98_GEOFANCY.pm added (loredo) - - feature: new module 70_XBMC.pm added (dbokermann) - - feature: new module 51_RPI_GPIO.pm added (klausw) - - bugfix: Dashboard: fixed bug identification an existing Weblink. - fixed bug dashboard_sorting check. Buttonbar can now placed on - top or bottom of the Dashboard. Dashboard is always edited out - the Room Dashboard. - - bugfix: VIERA: fixed bug related to set command remoteControl - - bugfix: ENIGMA2: improved compatibility for Fritzbox and old - Webif versions - - feature: readingsGroup: process events only if visible in browser, - allow
for line breaks in multi-reading lines - - feature: FLOORPLAN: Style4 (S300TH specific) now keeps its formatting - even with longpoll; Text "desiredTemperature" will now - be eliminated - for MAX devices. - - feature: HCS has now MAX Thermostat support - - change: integrated OWServer/OWDevice nonblocking and random start - time patches (justme1968 & Boris) - - feature: Add new module Dashboard - - change: ONKYO_AVR: transfer command database into separate packet - ONKYOdb.pm - - feature: ENIGMA2: bouquet support e.g. for named channels - - feature: Add new module ONKYO_AVR - - feature: SYSSTAT: allow (remote) monitoring via snmp, support - for monitoring windows systems and synology system temperature - - feature: New module LINDY_HDMI_SWITCH.pm added - - change: ENIGMA2: improved logging, default attributes for webCmd and - devStateIcon - - feature: ENIGMA2: support for option channels - - feature: mailcheck: decode non ascii subjet to utf-8, verify gpg signatures - - feature: PRESENCE: "statusRequest" command for lan-bluetooth mode - (collectord >= 1.4, presenced >= 1.1 required) - - feature: PRESENCE: new collectord package - - feature: devspec: removed range, added :FILTER and more general search - - feature: HUEBridge,HUEDevice: support for groups added - - feature: YAMAHA_AVR: new argument "toggle" for mute command - - feature: FB_CALLMONITOR: replace & to & at reverse search - - feature: new module 33_readingsProxy to make (a subset of) a reading - from one device available as a new device. can be used to - separate channels from 1-wire, EnOcean or SWAP multichannel - devices (by justme1968) - - change: improvements for OWDevice and OWServer (justme1968) - - feature: new attribute resolution for 1-wire temperature readings - (justme1968 & Boris) - - feature: new layout commands moveto, moveby and relative positioning - in 02_RSS.pm (Betateilchen & Boris) - - feature: FHEMWEB column attribute - - feature: new layout commands halign, valign, condition in 02_RSS.pm - (Betateilchen & Boris) - - bugfix: PRESENCE: Fix nonworking initialization in mode "lan-bluetooth" - - bugfix: fhem.pl: write-select to avoid blocking in inform/Event Monitor - - bugfix: fix issue with DST changes in 57_Calendar.pm - - feature: new module 36_LaCrosse.pm for LaCrosse IT+ temperature and - humidity sensors with a JeeLabs JeeLink as RF modem. - The matching JeeNode sketch can be found in - .../36_LaCrosse-LaCrosseITPlusReader.zip (by ohweh&justme1968) - - feature: YAMAHA_AVR: new attribute request-timeout. - - bugfix: YAMAHA_AVR: fix missing greater-than sign. Use different - Control-Tag name for RX-Vx75 series - - bugfix: PRESENCE: fixing not working re-initialization when - disabled attribute is set to 0 in lan-bluetooth mode - - feature: LightScene: added attribute lightSceneParamsToSave for - device specific configuration of config" - - feature: readings type added to weblink (justme1968) - - feature: offset and monotonic added to userReadings modifier (justme1968) - - feature: HUEDevice: support SVG icons for LWB001 living whites bulb - - feature: HUEDevice: support more than one bridge - - feature: updateInBackground global attribute - - feature: SYSSTAT: allow stateFormat - - feature: Module 70_VIERA supports now module 95_remotecontrol with own - layout for VIERA TV - - feature: InternalVal function added (like ReadingsVal) - - feature: new module speedtest to monitor internet connection speed with - speedtest-cli - - feature: new module "remotecontrol" to display a graphical remotecontrol - for any device - - feature: HUEDevice: new attribute color-icons to colored svg icons - - feature: FHEMWEB: longpoll is default now, longpollSVG (default off) added - - feature: HUEDevice: allow usage of openautomation svg icons - - feature: FHEMWEB: svg icons / iconPath / www/images/openautomation added - - feature: FHEMWEB: SVGcache attribute & clearSvgCache set command added - - feature: SYSSTAT: allow (remote) monitoring raspberry pi on cpu frequency - - feature: MANTAINER.txt added - - feature: PRESENCE: new mode "shellscript" to use own - scripts or binaries for presence recognition - - feature: YAMAHA_AVR: new set command to select scenes - - feature: PRESENCE: new attribute ping_count - - feature: userReadings may have a filter - - feature: HUEBridge: allow starting of bridge firmware update - - change: EnOcean: profile PM101 changed, old profiles FAH, FBH, FTF, SR04 - removed - - feature: TCM: new attr blockSenderID: - Block receiving telegrams with a TCM SenderID sent by repeaters - - feature: TCM: For TCM120 Transceiver now the transmission of RPS and 4BS - commands supported - - feature: EnOcean: Now all RPS / 1BS profiles, more than 90 4BS profiles and - some manufacturer specific profiles are supported - - feature: EnOcean: profiles (subType) are updated from EEP 2.1 to EEP 2.5 - - feature: FHEMWEB attribute roomIcons added - - feature: SYSSTAT: optionaly calculate geometric average of last 4 - temperature values - - feature: weblink details screen can be used to edit .gplot files - - feature: eventTypes module added, to help with FileLog details screen - - feature: FB_CALLMONITOR: new reverse search provider dasschnelle.at for - reverse search of austrian telephone numbers - - bugfix: event-on-change-reading in combination with event-change-interval - - change: HUEDevice: allow color preset buttons in webCmd - - feature: SYSSTAT: allow (remote) monitoring raspberry pi on chip temperature - - feature: HUEDevice: use webCmdFn for colorpicker - added jscolor for colorpicker - - feature: FHEMWEB: module specific summaryFn/detailFn + defineable webCmdFn - - change: ESA2000: adapted device detection , rename readings - - change: stucture triggers on each change, see event-on-change-reading - - feature: PRESENCE: new mode "function" to use own perl functions for - presence checks - - bugfix: fixing not-working FHEM restart, when a PRESENCE check is running - - bugfix: fixing memory overflow when "list" a PRESENCE definition - - bugfix: fixing dead PRESENCE definitions in case of timeouts - - bugfix: update: error while updating single files fixed. (M. Fischer) - -- 2013-04-08 (5.4) - - feature: updatefhem will be silently converted to update - - feature: FHEMWEB: save button replaced with the menu entry "Save config" - - feature: notify supports $NAME/$EVENT/$EVTPART0/etc. @/% is deprecated. - - feature: 93_DbLog extended to give more functions for the charting frontend. - This includes new queries for raw table data and also statistics, - which get sum/max/min/avg values from the database. - Documentation has been updated. - - feature: new module 31_LightScene to save and restore the state of a - group of lights and other actors - - feature: VIERA module added (by teevau) - - change: FHEMWEB: the first webCmd argument is no longer used by the - state-icon, this can be implemented by the new devStateIcon - - change: 30_HUEDevice: allow autodetection of bridge with hue portal - services - - feature: THRESHOLD Module by Damian - - change: 30_HUEDevice: use new devStateIcon feature to show device color - in room overview - - feature: added example Setup SQL and configuration for SQLite - - change: modified MySQL Setup SQL to use 512 characters in EVENT column - - feature: added new Javascript Frontend based on ExtJS (by Johannes) - - feature: new modules 30_HUEBridge and 31_HUEDevice for phillips hue and - smartlink devices (by justme1968) - - change: SYSSTAT: allow remote monitoring by ssh - - change: SYSSTAT: allow less frequent updates for diskusage - - feature: new module 32_SYSSTAT to monitor system load and disk usage - on linux FHEM hosts (by justme1968) - - feature: new Module 73_PRESENCE to make automatic presence detection of - mobile phones or other mobile devices (like tablets) via ping or - bluetooth checks (by M. Bloch) - - feature: new Module 98_Heating_Control to switch heatsinks automaticly - with a weekly profile (by D. Ortmann / T. Faust) - - feature: new Module 93_DbLog.pm for logging events into Databases. - Generating Plots with weblinks are supportet. - (by B. Neubert / T. Faust) - - feature: new Module 59_HCS.pm for monitoring heating valves (FHT, HM-CC-VD) - to contral a central heating unit. I thank Benjamin for his - support! (M. Fischer) - - feature: new Module 72_FB_CALLMONITOR for receiving telephone call events - (Markus) - - feature: new Module 71_YAMAHA_AVR.pm for controlling Yamaha AV receivers - over network (by Markus) - - feature: optional second parameter to fhem() to make it silent - - feature: autoloading commands, XmlList/etc renamed from 99 to 98. - - feature: FHEMWEB returns external files in chunks to save memory - - feature: commandref.html splitted: documentation is now appended to the - modules. - - change: introduced readingsBulkUpdate, readingsSingleUpdate - - change: added GPLv2 licensing information - - feature: FLOORPLAN added fp_setbutton attribute - - bugfix: FHEMWEB slider with min > 0 - - change: FHEMWEB CORS moved to options - - change: FHEMWEB closing old TCP connections - - change: FHEMWEB added "Associated with" to detail-screen (Uli) - - change: FHEMWEB added ETag headers (Matthias) - - change: FHEMWEB devStateIcon added - - change: HOWTO auf deutsch (ilmtuelp0815) - - change: 98_update.pm due a (probable) bug in perl, modules are no longer - loading automatically. A restart is required now! (M. Fischer) - - feature: 98_update.pm saves the statefile before an update (M. Fischer) - - feature: FHEMWEB longpoll reconnect (Matthias) - - bugfix: rename may overwrite other devices - - feature: FLOORPLAN longpoll (Matthias) - - feature: support for recurring events added in 57_Calendar.pm (Boris) - - feature: added support for OWL CM119,CM160 and CM180, energy sensors in - TRX_WEATHER using RFXtrx433 (Willi Herzig) - - feature: added support for KD101 smoke sensor (also set alert and pair) in - TRX_SECURITY using RFXtrx433 (Willi Herzig) - - change: changed dewpoint to work with event-on-change-reading and - technoline TX3TH (Willi Herzig) - - feature: new command fheminfo. Shows system informations. (M. Fischer) - - feature: added support for UV sensors in TRX_LIGHT using RFXtrx433 (Willi - Herzig) - - feature: added on-till and on-timer for set in TRX_LIGHT using RFXtrx433 - (Willi Herzig) - - feature: generate devices with hexcodes as state for unknown types in - TRX_ELSE using RFXtrx433 (Willi Herzig) - - feature: new modules 10_OWServer.pm and 11_OWDevice.pm to interface with - OWFS - - feature: stateFormat (readingsFn modules) and showInternalValues attributes - - feature: new readingsFn modules: FS20 CUL_WS HMS CUL_EM CUL_TX EnOcean ZWave - - change: BS, USF1000, ECMDDevice, Weather, dummy migrated to readingsFN - (Boris) - - feature: telnet client mode - - bugfix: FHEMWEB longpoll misses initial state change (HM: set_on vs. on) - - change: 20_OWFS.pm, 21_OWTEMP modules flagged as "deprecated". These - modules will be removed in a future release. Use OWServer / - OWDevice instead. (M. Fischer) - - feature: a lot of new features and known 1-wire slaves to OWServer / - OWDevice added (M. Fischer) - - feature: set-extensions (additional set commands) for FS20, EnOcean, ZWave - - feature: added new command 'notice'. (M. Fischer) - - change: update supports the display and confirmation of system messages - via the new notice command (M. Fischer) - - feature: added new set commands and basicauth to 49_IPCAM.pm (M. Fischer) - - feature: userReadings - - feature: average supports more than one value in combined readings (T:x H:y) - - feature: FHEMWEB serves arbitrary files from the www directory - - feature: FB_checkPw now works with a distinct fritzbox user - - bugfix: floorplan-correction for readings with longpoll. Requires local - change in css! - - feature: floorplan added js-extension from Dirk - - feature: hour resolution in SVG - - feature: ZWave support for MULTI_CHANNEL class - - feature: FHEMWEB: old-dir-support removed, image-indexing rebuilt, - smallscreen/touchpad moved to stylesheetPrefix, menuEntries - added, Extend devStateIcon, js setting of attr values in detail - screen, live slider update in detail and room view - - feature: added support for third-party packages to 98_update.pm (M. Fischer) - - feature: FBAHA/FBDECT for FRITZ!DECT devices - - feature: event-min-interval Attribute - -- 2012-10-28 (5.3) - - feature: added functions trim, ltrim, rtrim, UntoggleDirect, - UntoggleIndirect - - feature: added functions FB_mail, FB_WLANswitch - - rework: CUL_HM reworks with respect to protocol. additions for several - devices and commands - - feature: rfmode supports to listen to MAX if fw>1.46, 00_CUL.pm (Jens) - - feature: Status and length on cmdStack in webinterface for 10_CUL_HM - - feature: devicepair in 10_CUL_HM.pm supports unset - - feature: devicepair for single Button in 10_CUL_HM.pm (by MartinP) - - feature: new Modules 75_MSG.pm, 76_MSGFile.pm and 76_MSGMail.pm (by - Ruediger) - - feature: new Module 59_Twilight.pm to calculate current daylight - - feature: internal NotifyOrderPrefix: 98_average.pm is more straightforward - - feature: the usb command tries to flash unflashed CULs on linux - - feature: FHEMWEB: jsonp support, .holiday and .cfg added to Edit Files - - feature: SVG: filled area support, some ls/lw fixes - - feature: WOL (wake on lan) module added (by Matthias) - - feature: additional groups from /etc/groups are applied (Christopher) - - feature: updatefhem backup is using tar+gzip now - - feature: EIB: introduce Get, interpret received values upon defined model - (by datapoint types) (Maz) - - feature: NetIO230B module by Andy - - feature: Retaining configfile comments (not within a define statement) - - feature: EnOcean PM101 by Ignaz - - feature: FHEMWEB redirectCmds attribute added - - feature: CUL_TX minsecs attribute (by Arno) - - feature: webCmd in smallScreen added - - feature: TRX modules by Willi - - feature: FHEMWEB icons (by Joerg) - - feature: FHEMWEB console (same as inform timer) - - feature: remove dependency on Google::Weather, major rewrite (Boris) - - feature: started experimental interface implementation (fhem API v2) - (Boris) - - feature: sleep issued in at/notify/etc is not blocking fhem anymore - - feature: dummy got a setList attribute - - feature: new module 02_RSS.pm - - feature: at attribute alignTime added - - feature: FHEMWEB attribute values via dropdown, slider for dimmer - - feature: new attribute group for FHEMWEB (Boris) - - change: 11_FHT.pm, 50_WS300.pm, 59_Weather.pm migrated to readingsUpdate - mechanism (Boris) - - change: 59_Weather.pm migrated from Google to Yahoo Weather API (Boris) - - change: updatefhem modifications to support a clean install of fhem and - pgm2 installation, see commandref.html (M. Fischer) - - change: FHEMWEB support for the new www/pgm2 directroy added (M. Fischer) - - change: Makefile support for for the new www/pgm2 directroy and new - targets backup and uninstall added. More verbose output. (M. Fischer) - - change: backup separated from updatefhem to a new command (M. Fischer) - - feature: new command backup added (M. Fischer) new global attribute - added new global attribute added new global - attribute added - - feature: new module 57_Calendar.pm (Boris) - - feature: new parameter for updatefhem added (M. Fischer) new - global attribute added (M. Fischer) - - feature: optional telnet password added / telnet port is optional - - feature: holiday returns all matches, not only the first. - - change: CULflash separated from updatefhem to a new module (M. Fischer) - - feature: time and internet helper routines added to fhem.pl (Boris) - - change: separating common functions used by the FHEM modules into - *Utils.pm files from fhem.pl - - feature: portpassword and basicAuth may use evaluated functions - - feature: motd with SecurityCheck added - - feature: telnet module added, attr global port moved. allowfrom changed. - - feature: FhemUtils/release.pm for the new update process added. (M. - Fischer) - - bugfix: correct one-time relative at commands after reboot - - feature: ZWave added - - feature: module IPCAM added. (M. Fischer) - - feature: module HTTPSRV added (Boris) - - feature: module FLOORPLAN added (Uli Maass) - - bugfix: FHEMWEB: weblink with group attribute is shown together with other - elements - - feature: FHEMWEB: timepicker added - - feature: FHEMWEB: support for modul specific icons added (M. Fischer) - - -- 2011-12-31 (5.2) - - bugfix: applying smallscreen attributes to firefox/opera - - feature: CUL_TX added (thanks to Peterp) - - feature: TCM120/TCM310 + EnOcean parser added - - feature: eventMap enhanced - - bugfix: enabled logging for 59_Weather.pm (Boris) - - feature: language selection for 59_Weather.pm (Erwin) - - feature: .gplot files renamed from type to content - - bugfix: FS20 on-for-timer error reporting only in the logfile - - bugfix: FHEM2FHEM should work with CUL again, after syntax change - - feature: CUL directio mode (No Device::SerialPort needed) - - feature: FritzBox 7270 ZIP file - - bugfix: prevent fhem from stalling if telnet times out in 66_ECMD.pm - - feature: added postproc ability to classdef in 66_ECMD.pm (Boris, Heinz) - - feature: FHEMWEB longpoll mode, small fixes, tuned smallscreen mode - - feature: average module added - - change: moved the berliOS CVS repository to a sourceforge SVN repository - - feature: all FHEM modules have now a subversion id. - - bugfix: new perl compiled for the FritzBox 7270 - - feature: regexp1WontReactivate Attribute added - - bugfix: XmlList special handling - - bugfix: CUL_WS rain sensor corr1 fix - - feature: FHEMWEB stylesheet attribute repaced with stylesheetPrefix - - feature: notify attribute forwardReturnValue - - change: move JsonList from contrib to main-modules - - change: JsonList output optimized and more structured - - feature: FHEMWEB save button, smallscreen first screen fix - - feature: FHEMWEB encoding is now UTF-8, alias attribute is respected - - change: HTTPS certs directory moved from cwd into modpath - - feature: shutdown parameter restart added - - feature: usb scan/create command added (part of autocreate). - - feature: SaveAs added to FHEMWEB Edit-Files - - feature: EnOcean ElTako dimmer by Marc. - - feature: fhem is started as user fhem on the FB7390 - - -- 2011-07-08 (5.1) - - feature: smallscreen optimizations for iPhone - - feature: FHT8V rewrite (and moved from contrib into the FHEM directory). - - feature: PID rewrite (and moved from contrib into the FHEM directory). - - feature: FHEM2FHEM module - - bugfix: CUL get should not digest foreign events (fhtsoftbuffer) - - bugfix: S300TH sanity check won't allow negative temperatures. - - feature: decode CUL uptime - - feature: USB doc changes, FHZ initFS20_02/stopHMS parameters by Andreas. - - feature: CUL_HM for some HomeMatic devices. - - bugfix: HTML-Syntax check of the pgm2 output and documents (*.html) - - feature: added date alias for FHT80b (Boris) - - feature: attr may be a regexp (for CUL_IR) - - feature: Homepage moved from koeniglich.de/fhem to fhem.de - - feature: eventMap attribute - - feature: 64_ESA2000 added (by STefan/Gerd) - - feature: new modules 66_ECMD.pm and 67_ECMDDevice.pm for ethersex-enabled - devices and alike. - - bugfix: serial port setting on Linux broken if running in the background - - feature: IPV6 support, FHEMWEB basicAuth and HTTPS support - - feature: createlog added to the autocreate module - - feature: contrib/tcptee.pl added - - feature: HMLAN support - - feature: Fritzbox7390 image - - feature: pgm2 tablet support, included into the default configuration - - feature: TUL/EIB Support (by Maz) - - feature: updatefhem/CULflash - - feature: $value{} => Value(), $oldvalue => OldValue()/OldTimestamp() - - -- 2010-08-15 (5.0) - - **NOTE*: The default installation path is changed to satisfy lintian - - feature: KM271 - - bugfix: 99_SUNRISE_EL endless loop bug - - feature: CUL: optional baudrate spec in definition - - feature: CUL: sendpool attribute - - feature: CUL_HOERMANN module added - - bugfix: DST change: absolute at and relative sunrise fix - - feature: FHEMWEB javascript additions for SVG plots (click on lines/labels) - - feature: FHEMWEB smallscreen attribute (for smartphones) - - bugfix: the internal fhem() used in perl oneliners does not return a value - - feature: Dimmer function of X10 module changed to match FS20 - - feature: allow only meaningful readings (fill level > -5%) in USF1000 - - feature: device attr links in commandref.html - - bugfix: make BS known to CUL to avoid lost messages if both FHZ1300 and CUL - are connected, adjust matching rule - - feature: Copy&Paste in SVG - - feature: Debian/Ubuntu Package. Install-path changes to satisfy lintian - - feature: Allnet 3076/4027/4000T - - feature: RFXCOMM Module for Oregon Weatherstations - - feature: Davis VantagePro2 - - feature: ELV USB-WDE1 - - feature: addvaltriggers CUL attribute for adding RSSI as a trigger - - feature: CUL_WS sanity check for large temp differences. - -- 2010-03-13 (4.9) - - bugfix: changed the fhem prompt from FHZ> to fhem> - - bugfix: CUL_RFR fixes (chaining RFR's should work) - - bugfix: Path in the examples fixed (got corrupted) - - bugfix: PachLog fixes from Axel - - bugfix: HOWTO/Examples revisited for correctness - - bugfix: INITIALIZED, DEFINED, RENAMED, DELETED triggers - - feature: image weblinks from Stefan - - feature: OWFS support for passive Devices e.g. DS9097 (see commandref.html) - - bugfix: OWFS crash fhem with PGM2/3, xmllist (M.Fischer) - - bugfix: OWTEMP Defining a device without OWFS now fails (M.Fischer) - - bugfix: 21_OWTEMP.pm missing trigger fo notify/filelog (M.Fischer) - - feature: 99_getstate.pm get state from S555TH now (M.Fischer) - - feature: pgm3: automatic support for CUL_WS (S300TH) added (MartinH) - - bugfix: 21_OWTEMP.pm missing space within state logging (M.Fischer) - - bugfix: 21_OWTEMP.pm interval fixed (M.Fischer) - - bugfix: 21_OWTEMP.pm rewrite with errorcontrol and demo mode (M.Fischer) - - feature: ignore attribute - - bugfix: [pgm3] table-format on Android-Browser optimized - - feature: [pgm3] Skinable - change the colors. - - feature: [pgm3] Rooms possible for Webcam and Google-Weather - - bugfix: dummy/structure was listed twice in list and xmllist - - feature: 11_FHT.pm added new readings for warnings on battery, lowtemp, - window and windowsensor (M.Fischer) - - feature: autocreate.pm (create undefined RF devices, logs and plots) - - feature: on-for-timer added for X10 modules (Boris) - - bugfix: pgm3: Better check of availability of google-weather (MartinH) - - feature: pgm3: DBLog added for everything except UserDefs - (Gerhard Pfeffer / MartinH) - - feature: pgm2 style changes, SVG in background, optional compression - -- 2009-11-28 (4.8) - - bugfix: loosing data when sending FS20 messages in a group - - bugfix: better handling of disconnected CUN - - feature: softfhtbuffer added to CUL - - bugfix: pgm3: Pulldown-Menu FHTDEV with error-check (MartinH) - - feature: duplicate buffer added for multi-cul/-fhz setups - - feature: 20_OWFS.pm for 1-Wire via OWFS added (Martin Fischer) - - feature: 21_OWTEMP.pm for 1-Wire Digital Thermometer added (Martin Fischer) - - feature: CUL_FHTTK from Kai - - feature: pgm3: Google-Weather, Battery-Check, Log-View added (MartinH) - - feature: CUL_RFR (RF_ROUTING) added - - feature: Command save retains now the order of the old config file - - feature: List parameter added (list .* RFR_MSGCNT) - -- 2009-10-23 (4.7) - - bugfix: Reattached corrupted CUL device caused uninitialized message - - bugfix: CUL/HMS changes, HMS cleanup - - bugfix: EM/EMWZ/EMGZ set changed to work in FHEMWEB - - bugfix: Avoid unitialized in xmllist for corrupt readings, reporter Boris - - bugfix: Add binmode to 01_fhemweb.pm for windows - - bugfix: Uniform check for windows, enable CUL for windows. - - bugfix: CUL/HMS parsing patches from Peter - - bugfix: Fixes for Windows by Klaus - - bugfix: Another "rereadcfg" bugfix - - feature: Update to the current (1.27) CUL FHT interface - - feature: suppress inplausible readings from USF1000 - - feature: get time, fwrev, set reopen for CM11 (Boris 2009-09-12) - - bugfix: FHZ_ReadAnswer bugfix for Windows (Klaus, 20.8.2009) - - feature: CUL: device access code reorganized, TCP/IP support added (CUN) - - feature: Pachube module from Axel - - feature: dumpdef module from Axel in contrib - - feature: javascripting support in FHEMWEB (Klaus/Axel) - - feature: Module 09_BS.pm for brightness sensor added (Boris 2009-09-20) - -- 2009-07-03 (4.6) - - bugfix: fht actuator message clarification by Klaus - - feature: getstate command from Martin (25.12) - - bugfix: at drifts for relative timespecs - - bugfix: Add IODev to CUL/EM/CUL_WS/HMS/KS300 - - bugfix: FileLog get (pgm2 plots) wont find the first row in the file - - feature: 00_CUL: Answer CUR requests (status/time/fht) - - bugfix: support for second correction factor for EMWZ in 15_CUL_EM added - - feature: CUL further sets/gets added - - feature: Removed msghist for multiple FHZ handling, IODev attribute added - - bugfix: cut off string "(counter)" from fallback value in 13_KS300.pm - - feature: daily/monthly cumulated values for EMWZ/EMGZ/EMWM with 15_CUL_EM - - feature: 01_FHEMWEB.pm: multiple room assignments - - feature: 01_FHEMWEB.pm: fixedrange with optional [day|week|month|year] - - feature: 01_FHEMWEB.pm: attr title and label for flexible .gplot files - - feature: fhem.pl: attr global logdir used by wildcard %ld - - feature: do not block on disconnected devices (FHZ/CM11/CUL) - - bugfix: deleting at definition in the at command - - bugfix: deleting a notify/at/watchdog definition in a notify/at/watchdog - - feature: devspec =. E.g. set room=kitchen off; list disabled= - - feature: Common Module calling for CUL/FHZ/CM11 - - feature: Store CUL sensitivity info - - feature: avoid the "unknown/help me" message for unloaded devices - - feature: structure module for large installations - - feature: Cost Control in 15_CUL_EM (CostPerUnit, BasisFeePerMonth) - - feature: add counter differential per time in 81_M232Counter.pm - - feature: added USB compendium to documentation - - feature: pgm3: Documentation for pgm3 updated, HMS100CO added (and bugfix) - - bugfix: Defining a repeated at job in a sunrise/sunset at job fails - - bugfix: FHT "summer" fix (avoiding a lot of syncnow) - - feature: FHEMWEB modules added - - feature: holiday module + doc + example + holiday2we attribute - - bugfix: sunrise stuff fixed, doc missing - - feature: CUL FHT sending added - - bugfix: workaround to make M232 counter wraparound - - feature: sequence module added - - feature: Google Weather API support for FHEM (Boris 2009-06-01) - - feature: lazy attribute for FHT devices (Boris 2009-06-09) - - feature: tmpcorr attribute for FHT devices - - feature: CUL_EM generates an event for each of the READINGS - - feature: USF1000S support for FHEM added (Boris 2009-06-20) - - feature: CUL supports HMS (culfw >= 1.22 needed) - - feature: CUL shutdown procedure added - - feature: 14_CUL_WS: better error checking - - bugfix: webpgm2 multi line editing is working again - -- 2008-12-23 (4.5) - - bugfix: further 01_FHEMWEB cleanup - - feature: CUL support for FS20(r/w), FHT(readonly), KS300 and EM - - feature: command list outputs the device attributes too - - bugfix: rename bugs fixed - - bugfix: better integration of ReadyFn (Windows), slight overall speedup - - bugfix: Ignore/correct casing when autoloading modules - - bugfix: at is executed twice after a modify (rufus99, 2008-09-10) - - feature: FHT internal modifications (better protocol understanding) - - feature: add timestamp to inform - - feature: The strange stty settings in 00_FHEM.pm are optional - - bugfix: webpgm2 iPhone fix - - feature: fullinit and reopen commands for FHZ added (Boris 2008-11-01) - - bugfix: undefined NotifyFn in fhem.pl (Boris 2008-11-01) - - feature: new modules 00_CM11.pm and 20_X10.pm for integration of X10 - devices in fhem (Boris 2008-11-02) - - feature: X10 support for pgm3 (Boris 2008-11-02) - - bugfix: FHT short message warning - - bugfix: rereadconfig crashes with active webpgm2 connections (2008-11-13) - - bugfix: watchdog crash (2008-11-15) - - bugfix: Strange call for nonexistent MyCUL: ReadFn - - feature: webpgm2: gplot output goes to /tmp/gnuplot.err - - feature: devspec TYPE,DEF,STATE. e.g. list TYPE:FS20, set DEF:123 on - - bugfix: at schedules 2 events after the DST change (fix not verified) - - feature: commandref.html reorg. There are now device sections. - - feature: CUL / CUL_EM / CUL_WS documentation - - feature: do not block fhem when the CUR is disconnected - - bugfix: correct correction factors for EMEM in 15_CUL_EM.pm - - bugfix: more stable CUL initialization - - feature: reworked 15_CUL_EM.pm to account for timer wraparounds, more - readings added - - feature: speed gain through disabled refreshvalues query to all FHTs at - definition; if you want it back at a "set myFHT report1 255 - report2 255" command to the config file. - - feature: fhem commands may be added in modules. XmlList is external now. - - bugfix: rereadcfg from webpgm2 does not crash fhem.pl - - feature: jsonlist command from Martin (contrib/JsonList) - - feature: contrib/rotateShiftWork from Martin - - feature: contrib/fhem2speech from Martin - - bugfix: attributes of at devices disappear - - feature: attribute rainadjustment for KS300 (Boris 2008-12-17) - - bugfix: deleting at / watchdog while active creates an empty device - - feature: ExactId trigger added for wildcard HMS devices - -- 2008-08-04 (4.4) - - feature: RM100-2 battery empty warning (mare 23.07.08) - - feature: optimising the pgm2/SVG memory usage - - feature: autoloading FHEM modules - - bugfix: STATE/$value is carrying again the correct value - - feature: enhancing the Makefile and the documentation - - feature: 90_at is using now InternalTimer, subsecond precision added - - feature: HMS100-FIT added (01.01.08 by Peter and 22.01.08 by Juergen) - - feature: 91_watchdog added to handle the HMS100-FIT - - feature: cum_kWh/cum_m3 added to EMWZ/EMGZ (11.01.08 by Peter) - -- 2008-07-12 (4.3) - - bugfix: KS300 state was wrong after the STATE bugfix - - feature: HMS100CO (by Peter) - - feature: EMGZ (by Peter) - - feature: Generate warning if too many commands were sent in the last hour - - doc: linux.html: Introduction (Peter S.) - - feature: contrib/82_M232Voltage.pm (by Boris, 24.12) - - feature: delattr renamed to deleteattr (Rudi, 29.12) - - feature: defattr renamed to setdefaultattr (Rudi, 29.12) - - feature: device spec (list/range/regexp) for most commands implemented - - feature: %NAME, %EVENT, %TYPE parameters in notify definition - - feature: added 93_DbLog.pm, database logging facility (Boris, 30.12.) - - feature: webfrontend/pgm2 converted to a FHEM module - - bugfix: 99_SUNRISE_EL.pm: may schedule double events - - bugfix: 62_EMEM.pl, contrib/em1010.pl: correct readings for energy_kWh - and energy_kWh_w (Boris, 06.01.08) - - feature: global attr allowfrom, as wished by Holger (8.1.2008) - - feature: FHT: multiple commands, softbuffer changes, cmd rename, doc - - feature: EM1010PC: automatic reset - - feature: contrib/00_LIRC.pm (25.3, by Bernhard) - - bugfix : 00_FHZ: additional stty settings for strange Linux versions - - bugfix : pgm2 wrong temp summary for FHT's (reported by O.D., 16.4.2008) - - feature: FHEM modules may live on a filesystem with "ignorant" casing (FAT) - - feature: FileLog "set reopen" for manual tweaking of logfiles. - - feature: multiline commands are supported through the command line - - feature: pgm2 installation changes, multiple instances, external css - - feature: 87_ws2000.pm (thomas 10.05.08) - - contrib: ws2000_reader.pl Standalone decoder and server (thomas 10.05.08) - - doc: update fhem.html and commandline.html reflecting ws2000 and - windows installation(thomas 10.05.08) - - feature: add ReadyFn to fhem.pl in main loop to have an alternative for - select, which is not working on windows (thomas 11.05) - - feature: set timeout to 0.2s, if HandleTimeout returns undef=forever - - bugfix : WS2000:fixed serial port access on windows by replacing FD with - ReadyFn - - bugfix : FileLog: dont use FH->sync on windows (not implemented there) - - feature: EM, WS300, FHZ:Add Switch for Device::SerialPort and - Win32::SerialPort to get it running in Windows (sorry, untested) - - bugfix: FileLog undefined $data in FileLog_Get - - feature: fhem.pl check modules for compiletime errors and do not initialize - them - - feature: M232 add windows support (thomas 12.05.08) - - feature: add simple ELV IPWE1 support (thomas 12.05.08) - - feature: FileLog get to read logfiles. Used heavily by webpgm2 - - feature: webpgm2: gnuplot-scroll mode to navigate/zoom in logfiles - - bugfix: deleting FS20 device won't result in unknown device (Daniel, 11.7) - - feature: webpgm2 generates SVG's from logs: no need for gnuplot - - bugfix: examples corrected to work with current syntax - -- 2007-12-02 (4.2) - - feature: added archivedir/archivecmd to the the main logfile - - feature: 99_Sunrise_EL.pm (does not need any Date modules) - - bugfix: seldom xmllist error resulting in corrupt xml (Martin/Peter, 4.9) - - bugfix: FHT mode holiday_short added (9.9, Dirk) - - bugfix: Modifying a device from its own trigger crashes (Klaus, 10.9) - - feature: webpgm2 output reformatted - - feature: webpgm2 displaying multiple plots - - feature: FHT lime-protection code discovered by Dirk (7.10) - - feature: softwarebuffer for FHT devices (Dirk 17.10) - - feature: FHT low temperatur warning and offset (Dirk 17.10) - - change: change FHT state into warnings (Dirk 17.10) - NOTE: you'll get an undefined type state & - undefined type unknown_85 after upgrade. - - feature: Softwarebuffer code simplified (Rudi 22.11) - - bugfix: bug #12327 doppeltes my - - bugfix: set STATE from trigger - - bugfix: readings state vs STATE problem (xmllist/trigger) - - change: SUNRISE doc changed (99_SUNRISE.pm -> 99_SUNRISE_EL.pm) - - feature: support for the M232 ELV device (Boris, 25.11) - - feature: alternativ Quad-based numbers for the FS20 (Matthias, 24.11) - - feature: dummy type added (contrib/99_dummy.pm) - -- 2007-08-05 (4.1) - - doc: linux.html (private udev-rules, not 50-..., ATTRS) - - bugfix: setting devices with "-" in their name did not work - - doc: fhem.pl and commandref.html (notifyon -> notify, correction - of examples) - - feature: modify command added - - feature: The "-" in the name is not allowed any more - - bugfix: disabled notify causes "uninitialized value" (STefan, 1.5) - - bugfix: deleted FS20 items are still logging (zombie) (Gerhard, 16.5) - - bugfix: added FS20S8, removed stty_parmrk (Martin, 24.5) - - feature: added archivedir/archivecmd to the FileLog - - feature: added EM1010PC/EM1000WZ/EM1000EM support - - bugfix: undefined messages for unknown HMS devs (Peter, 8.6) - - bugfix: em1010 and %oldvalue bugs (Peter, 9.6) - - bugfix: SCIVT solar controller (peterp, 1.7) - - bugfix: WS300 loglevel change (from 2 to 5 or device specific loglevel) - - feature: First steps for a Fritz!Box port. See the fritzbox.html - -- 2007-04-14 (4.0) - - bugfix: deny at +{3}... (only +*{3} allowed), reported by Bernd, 25.01 - - bugfix: allow numbers greater then 9 in at +{} - - feature: new 50_WS300.pm from Martin (bugfix + rain statistics, 26.01) - - feature: renamed fhz1000 to fhem - - feature: added HISTORY and README.DEV - - doc: Added description of attribute "model". - - bugfix: delete the pidfile when terminating. (reported by Martin and Peter) - - feature: attribute showtime in web-pgm2 (show time instead of state) - - feature: defattr (default attribute for following defines) - - feature: added em1010.pl to the contrib directory - - doc: added linux.html (multiple devices, udev-links) - - REORGANIZATION: - - at/notify "renamed" to "define at/notify" - - logfile/modpath/pidfile/port/verbose "renamed" to "attr global xxx" - - savefile renamed to "attr global statefile" - - save command added, it writes the configfile and the statefile - - delattr added - - list/xmllist format changed - - disable attribute for at/notify/filelog - See HISTORY for details and reasoning - - added rename command - - webpgm2 adapted to the new syntax, added device specific attribute - and "set" support, gnuplot files are configurable, links to the - documentation added. - - bugfix: more thorough serial line initialization - -- 2007-01-25 (3.3) - - bugfix: 50_WS300.pm fix from Martin - - bugfix: pidfile does not work as expected (reported by Martin) - - bugfix: %U in the log-filename is wrong (bugreport by Juergen) - - feature: %V added to the log-filename - - feature: KS300 wind calibration possibility added - - feature: (software) filtering repeater messages (suggested by Martin) - - feature: the "client" fhz1000.pl can address another host - - bugfix: empty FHT battery is not reported (by Holger) - - feature: new FHT codes, e.g. month/day/hour/minute setting (by Holger) - -- 2007-01-14 (3.2) - - bugfix: example $state changed to $value (remco) - - bugfix: sun*_rel does not work correctly with offset (Sebastian) - - feature: new HMS100TF codes (Sebastian) - - feature: logging unknown HMS with both unique and class ID (Sebastian) - - feature: WS300: "Wetter-Willi-Status", rain_raw/rain_cum added, historic - data (changes by Martin & Markus) - - bugfix: broken rereadcfg / CommandChain after init - (reported by Sebastian and Peter) - - bugfix: sunrise_coord returned "3", which is irritating - -- 2007-01-08 (3.1) - - bugfix: delete checks the arg first "exactly", then as a regexp - - bugfix: sun*_rel does not work correctly with offset (Martin) - - feature: FAQ entry on how to install the sunrise stuff. - - feature: the inner core is modified to be able to handle - more than one "IO" device, i.e multiple FHZ at the same time, - or FHZ + FS10 + WS300. Consequences: - - "fhzdev " replaced with "define FHZ " - - "sendraw " replaced with "set raw " - - module function parameters changed (for module developers) - - set FHZ activefor dev - - select instead sleep after sending FHZ commands - - the at timer is more exact (around 1msec instead of 1 sec) - - ignoring FS20 device 0001/00/00 - - feature: contrib/serial.pm to debug serial devices. - - feature: WS300 integrated: no external program needed (Martin) - - feature: updated to pgm3-0.7.0, see the CHANGELOG at Martins site - -- 2006-12-28 (3.0) - - bugfix: KS300: Make the temperature negative, not the humidity - - bugfix: generate correct xmllist even with fhzdev none (Martin, 12.12) - - feature: one set command can handle multiple devices (range and enumeration) - - feature: new FS20 command on-till - - feature: perl: the current state is stored in the %value hash - - feature: perl: sunset renamed to sunset_rel, sunset_abs added (for on-till) - - feature: perl: isday function added - - feature: follow-on-for-timer attribute added to set the state to off - - bugfix: the ws300pc negative-temp bugfix included (from Martin Klerx) - - feature: version 0.6.2 of the webpgm3 included (from Martin Haas) - -- 2006-11-27 (2.9a) - - bugfix: FileLog+Unknown device generates undefined messages - - bugfix: trigger with unknown device generates undefined messages - -- 2006-11-19 (2.9) - - bugfix: fhz1000.pl dies at startup if the savefile does not exist - - bugfix: oldvalue hash is not initialized at startup (peter, Nov 09) - - feature: Notify reorganization (requested by juergen and matthias) : - - inform will be notified on both real events and set or trigger commands - - filelogs will additionally be notified on set or trigger commands - - the extra_notify flag is gone: it is default now, there is a - do_not_notify flag for the opposite behaviour. - - feature: at timespec as a function. Example: at +*{sunset()} - commandref.html and examples revisited. - - feature: 99_SUNRISE.pm added to use with the new at functionality - (replaces the old 99_SUNSET.pm) - - feature: webpgm2 "everything" room, at/notify section, arbitrary command - - bugfix: resetting the KS300 - - feature: updated ws300pc (from martin klerx, Nov 08) - - bugfix: parsing timed commands implemented => thermo-off,thermo-on and - activate replaced with timed off-for-timer,on-for-timer and - on-old-for-timer (reported by martin klerx, Nov 08) - - feature: pidfile (requested by peter, Nov 10) - - bugfix: function 81 is not allowed - -- 2006-11-08 (2.8) - - feature: store oldvalue for triggers. perl only. requested by peter. - - feature: inform cmd. Patch by Martin. There are many Martins around here :-) - - bugfix: XML: fix & and < and co - - bugfix: Accept KS300 negative temperature values - - change: the FS20 msg "rain-msg" is called now "activate" - - feature: start/stop rc script from Stefan (in the contrib directory) - - feature: attribute extra_notify: setting the device will trigger a notify - - feature: optional repeat count for the at command - - feature: skip_next attribute for the at command - - feature: WS300 support by Martin. Check the contrib/ws300 directory. - - bugfix: 91_DbLog.pm: retry if the connection is broken by Peter - - feature: Martin's pgm3-0.5.2 (see the CHANGELOG on his webpage) - - feature: RRD logging example by Peter (in the contrib/rrd directory) - -- 2006-10-03 (2.7) - - bugfix: Another try on the > 25.5 problem. (Peters suggestion) - - feature: 99_ALARM.pm from Martin (in the contrib directory) - - feature: HMS100TFK von Peter P. - - feature: attribute loglevel - - feature: attribute dummy - - feature: attr command documented - - feature: the current version (0.5a) of the pgm3 from Martin. - -- 2006-09-13 (2.6a) - - bugfix: the FHT > 25.5 problem again. A never ending story. - -- 2006-09-08 (2.6) - - bugfix: updated the examples (hint from Juergen) - - bugfix: leading and trailing whitespaces in commands are ignored now - - feature: making life easier for perl oneliners: see commandref.html - (motivated by STefans suggestions) - - feature: include command and multiline commands in the configfiles (\) - - bugfix: web/pgm2 KS300 rain plot knows about the avg data - - bugfix: the FHT > 25.5 problem. Needs to be tested. - - feature: log unknown devices (peters idea, see notifyon description) - - feature: HMS wildcard device id for all HMS devices. See the define/HMS - section in the commandref.html for details. - NOTE: the wildcard for RM100-2 changed from 1001 to 1003. - (peters idea) - - feature: rolwzo_no_off.sh contrib file (for those who were already closed - out by automatically closing rollades, by Martin) - - feature: the current version (0.4.5) of the pgm3 from Martin. - -- 2006-08-13 (2.5) - Special thanks to STefan Mayer for a lot of suggestions and bug reports - - If a command is enclosed in {}, then it will be evaluated as a perl - expression, if it is enclosed in "" then it is a shell command, else it is - a "normal" fhz1000 command. - "at" and "notifyon" follow this convention too. - Note: a shell command will always be issued in the background. - - won't die anymore if the at spec contains an unknown command - - rereadcfg added. Sending a HUP should work better now - - escaping % and @ in the notify argument is now possible with %% or @@ - - new command trigger to test notify commands - - where you could specify an fhz command, now you can specify a list of - them, separated by ";". Escape is ;; - - KS300 sometimes reports "negative" rain when it begins to rain. Filter - such values. israining is set when the raincounter changed or the ks300 - israining bit is set. - - sleep command, with millisecond accuracy - - HMS 100MG support by Peter Stark. - - Making FHT and FS20 messages more uniform - - contrib/fs20_holidays.sh by STefan Mayer - (simulate presence while on holiday) - - webfrontends/pgm4 by STefan Mayer: fs20.php - - KS300 avg. monthly values fixed (hopefully) - - deleted undocumented "exec" function (you can write it now as {...}) - -- 2006-07-23 (2.4) - - contrib/four2hex (to convert between ELV and our codes) by Peter Stark - - make dist added to set version (it won't work in a released version) - - reload function to reload (private) perl modules - - 20_FHT.pm fix: undef occures once without old data - - "setstate comment" is replaced with the attr command (i.e. attribute). - The corresponding xmllist COMMENT tag is replaced with the ATTR tag. - Devices or logs can have attr definitions. - - webfrontend/pgm2 (fhzweb.pl) updated to handle "room" attributes(showing - only devices in this room). - - version 0.4.2 of webfrontend/pgm3 integrated. - - contrib/ks300avg.pl to compute daily and monthly avarage values. - - the 40_KS300.pm module is computing daily and monthly avarages for the - temp/hum and wind values and sum of the rain. The cum_day and cum_month - state variables are used as helper values. To log the avarage use the - .*avg.* regexp. The regexp for the intraday log will trigger it also. - - Added the contrib file garden.pl as a more complex example: garden - irrigation. The program computes the time for irrigation from the avarage - temperature reported by the ks300-2. - - Enable uppercase hex codes (Bug reported by STefan Mayer) - - Renamed the unknown_XX FHT80b codes to code_XXXXXX, this will produce - "Undefined type" messages when reading the old save file - - RM100-2 added (thanks for the codes from andikt). - -- 2006-6-22 (2.3) - - CRC checking (i.e. ignoring messages with bad CRC, message on verbose 4) - - contrib/checkmesg.pl added to check message consistency (debugging) - - FHT: unknown_aa, unknown_ba codes added. What they are for? - - Empty modpath / no modpath error messages added (some user think modpath is - superfluous) - - Unparsed messages (verbose 5) now printed as hex - - Try to reattach to the usb device if it disappears: no need to - restart the server if the device is pulled out from the USB socket and - plugged in again (old versions go into a busy loop here). - - Supressing the seldom (ca 1 out of 700) short KS300 messages. - (not sure how to interpret them) - - Added KS300 "israining" status flag. Note: this not always triggers when it - is raining, but there seems to be a correlation. To be evaluated in more - detail. - - notifyon can now execute "private" perl code as well (updated - commandref.html, added the file example/99_PRIV.pm) - - another "perl code" example is logging the data into the database - (with DBI), see the file contrib/91_DbLog.pm. Tested with an Oracle DB. - - logs added to the xmllist - - FHT80b: Fix measured-temp over 25.5 (handling the tempadd messages better) - -- 2006-05-20 (2.2) - - FHZ1300 support verified (+ doc changes) - - KS300 support added (with Temperature, Humidity, Wind speed, Rain). - Not verified/undecoded: subzero temp, weak battery, Is-raining flag, - wind speed > 100km/h - - webpgm2 log fix for "offed" FHT devices (with no actuator data) - - webpgm3 upgrade (by Martin Haas, see webpgm/pgm3/docs/CHANGES for details) - - HMS logging/state format changed to make it similar to KS300 - - added HMS100WD (thanks to Sascha Pollok) - - ntfy/logging changed to be able to notify for multiple attributes - arriving in one message - - central FHTcode settable (see commandref.html) - - optionally listen for non-local requests (port global) - - unknown logging - - FAQ - -- 2006-04-15 (2.1) - - webfrontend/pgm2 changes: - - make it work on Asus dsl-routers (no "use warnings") - - css/readonly configurable - - Formatting for HMS data - - comments can be added to each device (setstate comment:xxx) - - testbed to dry-test functionality (test directory) - - added an empty hull for the KS300 weather module - - added undocumented "exec" function to call arbitrary program parts - for debugging. Example: exec FhzDecode("81xx04xx0101a0011234030011"); - - webfrontend/pgm3, contributed by Martin Haas - - fixed pgm1: changing values should work now - -- 2006-04-02 (2.0) - - XmlList and webfrontend/pgm1 programs from Raoul Matthiessen - - list tries to display the state and not the last command - - Both log facilities (FileLog and Log) take wildcards - (week, year, month, etc) to make logfile rotating easier - - webfrontend/pgm2 - -- 2006-02-12 (1.9b) - - Bugfix: Fixing the same bug again (thanks to Martin) - -- 2006-02-12 (1.9a) - - Bugfix: wrong rights for HMS and wrong place for readonly - (thanks to Juergen) - -- 2006-02-10 (1.9) - (aka as the Juergen release) - - The FHZ1300 is reported to work - - Bugfix: spaces before comment in the config file should be ignored - - added FS20STR codes to 10_FS20.pm - - names restricted to A-Za-z0-9.:- (especially _ is not allowed) - - delete calles now an UndefFn in the module - - implementation of FS20 function group/local master/global master - - the list command tells you the definition of the device too - -- 2006-01-05 (1.8) - - Bugfix: detailed FS20 status was not set from external event - - Bugfix: setstate for FS20 returned the last state set - - Bugfix: undefined FS20 devices (can) crash the server - - HMS module added by Martin Mueller - (currently supporting the HMS100T & HMS100TF) - - Log modules added, the first one being a simple FileLog - (inspired by Martin Mueller) - - A little gnuplot script to display temperature and actuator changes - -- 2006-01-04 (1.7) - - the at command can be used to execute something repeatedly with * - - ntfy can filter on device or on device+event with a regexp - - checking the delete and notify regexps if they make sense - - the FHT init string is now a set command (refreshvalues) - - shutdown saves the detailed device information too - -- 2006-01-03 (1.6) - - signal handling (to save the state on shutdown) - - module FHZ addded (for the FHZ1000PC device itself) - - added the get function (to make the initialization prettier) - - the module ST was renamed to FS20 - - FS20 timer commands added - - modules command removed (we are loading everything from the modpath - directory) - - FHT80b module added (yes, it is already useful, you can set - and view a lot of values) - - documentation adapted - - Added a TODO file - -- 2005-12-26 (1.5) - - "modularized" in preparation for the FHT80B -> each device has a type - - added relative "at" commands (with +HH:MM:SS) - - multiple commands on one line separated with ; - - sleeping 0.22 seconds after an ST command - - some commands/syntax changed: - - switch => set - - device => fhzdevice - - define ... => define ... - - the state of the devices and the at commands are saved - - at start always sending a "set 0001 00 01" to enable the FHZ receiever. - This is a workaround. - - doc rewrite, examples directory - -- 2005-11-10 (1.4) - - Reformatting the package and the documentation - - New links - -- 2005-10-27 (1.3) - - Bugfix: multiple at commands at the same time. + - feature: 70_PushNotifier added line break in Messages (xusader) + - feature: readingsGroup: added valuePrefix and valueSuffix attributes + added collapsed/collapsible to visibility attribute + added visibility command + - bugfix: FB_CALLMONITOR: fixing not working company numbers + reverse search for search.ch + - bugfix: 70_PushNotifier repair set function (xusader) + - bugfix: PRESENCE: fixing not working timer, when using set [...] statusRequest + - bugfix: FB_CALLMONITOR: fixing reverse search for klicktel.de + - feature: new module 52_I2C_MCP342x.pm added (klausw) + - feature: SYSMON: read cpu temp on FritzBox + - feature: ios7smallscreenstyle.css: table width based on screen width, new + header, links colored in detail view + - feature: new module FRITZBOX: controls Fritz!Box router and Fritz!Fon + - feature: new module 52_I2C_EEPROM.pm added (klausw) + - feature: readingsGroup: added ! flag, + added visibility and cellStyle attributes + - feature: new module 52_I2C_MCP23008.pm added (klausw) + - feature: new module 98_logProxy.pm added (justme1968) + - change: 66_ECMD: ReadyFn added (fixes issue under Windows) + - change: 02_RSS: use a GUID in RSS; urlq source for img command + - feature: 70_PushNotifier improve usebility, configuration without cURL (xusader) + - bugfix: SYSMON: prevent empty line im log by userReadings + - feature: 10_IT empfang (by bjoernh) + - bugfix: PRESENCE: fix race condition, when delete disabled attribute and + PRESENCE does not start to scan. + - feature: OPENWEATHER: captures weather forecast from API of www.wetter.com + - fhem 5.6 released + +- 2014-11-09 (5.6) + - bugfix: FB_CALLMONITOR: fixing race condition of missing events while + performing multiple calls + - feature: PROPLANTA: captures weather forecast from web page www.proplanta.de + - feature: 15_CUL_EM added attribute maxPeak (arnoaugustin) + - bugfix: 10_IT changed "setstate" to avoid eventMap errors (arnoaugustin) + - feature: new module 37_harmony.pm added (justme1968) + - change: WMBUS: use _ instead of : as readings separator, better support for EnergyCam + - feature: new module 23_KOSTALPIKO added (john) + - feature: new module 98_HourCounter added, 99_UtilsHourCounter.pm added to contrib (john) + - added: MYSENSORS: connect to serial or Ethernet MySensors Gateway + - added: MYSENSORS_DEVICE: represent a MySensors sensor- or actor node + - feature: global ATTR/DELETEATTR/MODIFIED events + - feature: 55_GDS.pm - attr disable added + - bugfix: SYSMON: prevent endless loop at startup with 'disable' attribute + - feature: SYSMON: added FritzBox informations: DSL rate, DSLAM sync time, count of CRC an FEC + - bugfix: SYSMON: unwanted characters in dsl info lines + - change: 57_Calendar: process continuation lines, get/set syntax checks + - bugfix: SYSMON: fix availability of cpu/kernel_max + - bugfix: SYSMON: numeric check + - change: 59_Weather: change icons for conditions 31, 34, 36 + - added: MQTT: connect fhem with mqtt + - added: MQTT_BRIDGE: bidirectional mapping of existing fhem-device to mqtt-topic + - added: MQTT_DEVICE: fhem-device that can be controlled by and publishes to mqtt + - added: I2C_LCD: module to drive PCF8574T based LCD connected via I2C + - added: I2C_DS1307: module to read time and date from DS1307 connected by i2c + - added: OWX_ASYNC: asynchronous, non-blocking version of OWX for DS2480, DS9097 and FRM + - feature FRM: work as physical IODev for I2C_XXX modules + added: FRM_ROTENC: read rotary-encoders with FRM + added: FRM_RGB: control rgb-leds with FRM + added: FRM_STEPPER: control stepper-motors with FRM + added: FRM_AD: analog input for FRM + added: FRM_PWM: analog (pwm) output for FRM + added: FRM_SERVO: control servo-motors with FRM + - added: FRM_IN: digital input for FRM + - added: FRM_OUT: digital output for FRM + - added: FRM: connect Arduino with firmata to fhem + - change: 57_Calendar: line parsing rewritten, care for missing + modification timestamps + - change: SYSMON: support userReadings in SYSMON_ShowValues + - change: 59_Weather: change icon for condition clear to sunny.png + - bugfix: 57_Calendar: calendar event anymore in modeAlarmed if started + - feature: 57_Calendar: deal with non-existent end times + - bugfix: SOMFY: fix non-working on/off-for-timer methods + made positioning attributes optional + - feature: SOMFY: support for exact positioning (one-time setup of run times required) + support for parse()-function, requires newest CULFW. + - feature: userattr is now also device attribute + - feature: ZWave: Fibaro_FGRM222 MANUFACTURER_PROPRIETARY class + - feature: sequence: reportEvents attribtue added + - feature: SYSMON: RAM and SWAP Readings on OSX + - change: 34_NUT: removed calculation of values. Use userReadings instead. + removed autogeneration of attr model and serNo. + - feature: SYSMON: improvement: support network information (IP, IPv6) on german linux + - feature: Synology DiskStation NAS basic spk file creation + - change: 34_NUT: readingFnAttributes added; creation of units deleted; + changed attr asReadings to use comma instead of space + - bugfix: SYSMON: crash on FritzBox + - bugfix: 34_NUT: fixed possible buffer overflow, rewrote reception of data + - bugfix: SYSMON: idletime on multicore, warnings + - change: 09_CUL_FHTTK.pm: modified set option for sync, open and closed + - feature: SYSMON: HTML/Text output for SYSMON-CloneDummies + - feature: SYSMON: Method for titled HTML/Text output + - added: 34_NUT.pm (maintainer: creideiki) + - feature: SYSMON: added new reading: perl_version + - feature: add toggle to SetExtensions (introduced for ZWave) + - bugfix: plotEmbed FHEMWEB attribute (fix for an iOS8 bug) + - feature: SHC: support for analog inputs (EnvSensor) and new device + RGB_Dimmer added (rr2000) + - feature: PRESENCE: MAC address support for mode fritzbox (by Markus M.) + - bugfix: PRESENCE: fixing presence detection in mode fritzbox with new + Fritz!OS 6.20 (by Markus M.) + - feature: FB_CALLMONITOR: reverse-search attribute is now providing all + possible values, which are selectable (via fhemweb_multiple.js). + see commandref for all possible values + - feature: speed up through caching of postproc and regex in ECMDDevice + - bugfix: fixed handling of autocreation for 10_OWServer.pm + - feature: option to cope with partial messages in ECMD/ECMDDevice + - bugfix: SOMFY: add module to CUL client list, to set IODev automatically + - feature: sequence: triggerPartial Attribute added + - feature: 36_JeeLink: changed flash command to use fhem firmware + directory (by HCS) + - feature: 70_ENIGMA2: new attribute lightMode for old/slow devices + limited restricted functionality + - added: 98_CustomReadings.pm (maintainer: HCS) + - change: 98_Text2Speech.pm: fix a problem with microseconds in time() + by using mp3-templates or playing mp3 directly + - feature: state definition and split attribute added to 66_ECMD, + 67_ECMDDevice + - FHEMWEB: JavaScripts and CssFiles attributes added + - change: avoid updating weather information on get (59_Weather.pm) + - change: removed noshutdown=0 for HTTP connections made in 57_Calendar.pm + and 59_Weather.pm to address issues when FHEM is behind + a web proxy + - feature: update rewritten, restore added + - feature: enabled JavaScript in 02_RSS to support WebViewControl + - added: new module 36_WMBUS.pm (kaihs) Wireless M-Bus + - feature: SYSMON: aded new plots (power infos for cubietruck) + - feature: SYSMON: aded new readings for each network interface: ip and ip6 + - feature: SYSMON: aded power supply informations to the text output method + - feature: SYSMON: power supply informations (ac, usb, battery) + - feature: added 70_PushNotifier.pm + - feature: 70_VIERA: Add parameter "HDMI1" - "HDMI4" for command remoteControl + to select HDMI input directly. + Add command "input" to select a HDMI port, TV or SD-Card + as source + - bugfix: LevelSender: Version 1.0.5: Could not get compiled by the Arduino + IDE + - feature: PRESENCE: new event "error" and "timeout" for state reading to + indicate a non successful check + - bugfix: 70_Jabber: fixed UTF8 encoding/decoding of messages + - feature: 10_OWServer autocreate coexists with OWXXX modules (Boris & + ntruchsess) + - feature: added 36_Level.pm + - feature: netatmo: added plz support for public stations + - change: 70_ENIGMA2: keep reading for recordings up-to-date during standby + - bugfix: 98_Text2Speech: - playing mp3files directly, eg: :ring.mp3: + - playing any mp3file only text + - feature: FB_CALLMONITOR: new reading "direction" to differentiate + between incoming and outgoing call. + - feature: FB_CALLMONITOR: all informational readings about a call will be + triggered for each call event + (call, ring, connect and disconnect) + - feature: mailcheck: allow user and pssword as perl expression + - feature: netatmo: support for public stations + - feature: PRESENCE: new set command "power" to execute a FHEM command + which can power on or off the checked device (given via attribute) + - feature: readingsGroup: added valueColumn attribute + - feature: readingsGroup: added ...,@,... argument format + - feature: 52_I2C_PCF8574.pm: added attribute OnStartup + 52_I2C_PCA9532.pm: added attribute OnStartup, + added attribute OutputPorts as substitute for InputPorts + 51_RPI_GPIO.pm: changed access to gpio via userspace by default + (for BBB and Cubie), access via gpio utility as fallback + - feature: PIONEERAVR: new attribute: checkConnection + - change: do no parse empty lines in 57_Calendar.pm + - bugfix: 10_SOMFY.pm: save enc-key and rolling-code as reading instead of + attribute to prevent loss of control after FHEM restart. + - feature: new module 10_SOMFY.pm to support Somfy RTS blinds + - bugfix: 70_PIONEERAVR.pm: fix for STATE if connection is lost + - bugfix: 37_SHC.pm: Move xml file under subdir lib, otherwise it won't be + deployed during update + - bugfix: 70_PIONEERAVR.pm: player commands are now available for more inputs + "play" was not in the drop down list of available set commands + check every 120s if the data connection to the Pioneer AV + receiver is still up + check if we get a reply from the Pioneer AV receiver not later + than 3s after a command was sent + fix for alias names of inputs + more input presets (spotify, mhl, hdmi7, hdmi8), inputs are now + queried from 1 - 59 + 71_PIONEERAVRZONE.pm: bugfix:logging, set input + - change: 00_RPII2C.pm: hardware access changed to ioctl and syswrite/read + SMBus module not needed anymore but still usable, see attribute + useHWLib possibility to swap I2C-0 to P5 for Rev. B raspberries + via attribute swap_i2c0 (not tested yet) + - feature: 70_ENIGMA2: add attribute + remotecontrol=[standard,advanced,keyboard] + - bugfix: 70_PIONEERAVR.pm and PIONEERAVRZONE.pm: added "use SetExtensions", + commandref updates fixed RC_layout + - feature: new modules 70_PIONEERAVR.pm and PIONEERAVRZONE.pm + - feature: FLOORPLAN has new style 7 to display commands only. + - added: 89_HEATRONIC.pm (heikoranft) + - bugfix: duration parsing of calendar events in 57_Calendar.pm fixed + - feature: LightScene: added followDevices attribute + - feature: non-blocking retrieval of data in 59_Weather.pm (Boris & herrmannj) + - feature: new modules 37_SHC.pm and 37_SHCdev.pm added (rr2000) + - feature: 36_EMT7110: added this new module + - feature: 36_JeeLink: added initCommands attribute and flash command (by HCS) + - feature: SYSMON: DECT Temperatur + - bugfix: SYSMON: prevent some warnings + - change: SYSMONremoved support for old format of filesystem reading + - change: moved 98_openweathermap.pm to contrib + moved 98_geodata.pm to contrib + moved 55_BBB_BMP180.pm to contrib + - change: honor DURATION in 57_Calendar.pm + - bugfix: YAMAHA_AVR: don't let FHEM hang anymore, when the receiver + is not reachable + - change: 55_GDS.pm: use Blocking.pm for retrieval of large files + - change: YAMAHA_BD: make YAMAHA_BD more performant by using non-blocking + HTTP request (from HttpUtils.pm) + - added: YAMAHA_BD: new set command trickPlay and more remoteControl + commands. new reading trickPlay + - added: new module 98_statistics.pm: hourly, daily, monthly, yearly + statistics for min/avg/max/delta/duration of selected readings + - added: new module 52_I2C_MCP23017.pm (klausw) + - feature: Dashboard Configuration-Dialog for Tabs + - feature: new module 33_readingsHistory.pm added (justme1968) + - feature: new command copy (justme1968) + - feature: enabled GIF, PNG and JPG as background image formats, enabled + relative font size changed and perl specials for font size + in 02_RSS.pm + - feature: YAMAHA_AVR: new set commands and readings for controlling + the sound output behavior (Enhancer, DSP and straight + output) and sleep timer. For details, see commandref. + - bugfix: configdb filemove not working after previous changes + - change: IMPORTANT CHANGES TO configDB! + changed: all files will be imported as binary + changed: all existing textfiles will be moved to binary + removed: command binfileimport + added: sorted write and read of configuration data + - bugfix: SYSMON: css class name (sysmon) + - feature: option to determine the number of icons from WeatherAsHtml + - feature: DbLog: (thanks to betateilchen) + * added new global modules function $hash->{DbLog_splitFn} + to let split the generated events by the own module + into readingsname, value and unit + * added SVG_sampleDataFn + * added FW_detailFn + - added: new module contrib/97_SprinkleControl.pm (tobiasfaust) + - added: new module contrib/98_Sprinkle.pm (tobiasfaust) + both modules helps to control the sprinkles in your garden + --> take a look to the Wiki-Article + http://www.fhemwiki.de/wiki/Bew%C3%A4sserungssteuerung + - feature: FB_CALLMONITOR: new reading "missed_call_line" indicating + the line number which received the missed call + - feature: YAMAHA_AVR current* readings will be erased in case they + not applicable + - feature: YAMAHA_AVR currentTitle available for TUNER + - feature: new Method: SYSMON_getValues([desired keys]) + - feature: JSONMETER: hourly statistics + - feature: configdb: new command search + - feature: LUXTRONIK2: estimation of electrical power consumption, + considers time depending tariffs (activeTariff) + - added: configDB functions for handling binary files + - feature: 02_RSS.pm: alpha channel for colors + - feature: JSONMETER: time depending tariffs added (activeTariff) + - updated: codemirror version 3.24 + - feature: new module 35_SWAP_0000002200000008 for panstamp + indoor multi sensor board with tft + - added: new module 36_EC3000.pm (justme1968) + - feature: IT: added support for set-extensions (justme1968) + - added: new modules 10_Itach_IR and 88_Itach_IRDevice to + use Itach WF2IR or IP2IR to be used as universal + infrared remotecontrol + - added: new module 51_I2C_TSL2561.pm (kaihs) + - added: new module 02_FRAMEBUFFER.pm (kaihs) + - feature: SYSMON: many FritzBox specific readings: + wlan_state, wlan_guest_state, internet_ip, internet_state, + night_time_ctrl, num_new_messages, fw_version_info + - feature: configDB: added command fileshow + - feature: configDB: added commands filelist and filedelete + - feature: configDB: added commands fileimport and fileexport + - feature: 36_JeeLink: added LaCrosse, ETH200comfort, CUL_IR, + HX2272 and FS20 modes from ulli + added AliRF + added Clients and MatchList attribute + - feature: 02_RSS.pm: HTTPS enabled, png as image type added, autofreshing + HTML page with image map added + - feature: ECMD and ECMDDevice completely reworked, see + http://forum.fhem.de/index.php/topic,21515.0.html + - feature: new layout command rect and new attribute bgcolor for RSS + - added: new module 55_weco.pm (betateilchen) + - added: new module 70_Jabber.pm (BioS) + - bugfix: Dashboard: use "loadScript" for load JavaScripts + - feature: new module 00_NetzerI2C.pm, 51_Netzer.pm added (klausw) + - feature: new command reload for 57_Calendar.pm forces cleanup + - changed: small update to the documentation of recurring events in + 57_Calendar.pm + - bugfix: PRESENCE: fixing wrong presence state for mode lan-ping + when device is unreachable + - feature: 10_EnOcean: new EEP profiles: D2-01-00 - D2-01-11 (VLD) + - changed: 00_TCM/10_EnOcean: learning mode (teach-in / teach-out) changed + and extended + - added: new module 10_UNIRoll.pm (c-herrmann) + - feature: cloneDummy: new attribut cloneIgnore + - feature: cloneDummy: new optional parameter [reading] + - bugfix: Dashboard: dashboard_showfullsize not applied in room "all" + - feature: new module 98_PID20.pm added (John / betateilchen) + - feature: new module 00_RPII2C.pm, 52_I2C_PCA9532.pm, 52_I2C_PCF8574.pm, + 52_I2C_SHT21.pm added (klausw) + - change: module 71_LISTENLIVE.pm moved to contrib + module 23_WEBTHERM.pm moved to contrib + - change: module 98_PID.pm moved to contrib as preparation for + next major replace. Replaced by 98_PID20.pm (John/betateilchen) + - change: openweathermap: added set command "clear" + - change: MAX: interpret SetTemperature command from WT to HT + - feature: MAX: retry packets 3 times if missing an ack + - feature: new module 98_cloneDummy.pm added (Joachim) + - feature: STACKABLE_CC (busware.de device for the RPi) added + - feature: configdb export/import added for data security (betateilchen) + - feature: new module 38_netatmo.pm added (justme1968) + - change: 09_CUL_FHTTK.pm: clean up code to avoid "Use of uninitialized + value in concatenation.." + - change: 09_CUL_FHTTK.pm: extend module list to FHT80TF and FHT80TF-2 + and update of documentation (matscher) + - feature: disabledForIntervals attribute added for at/notify/watchdog + - feature: jsonlist2 added, jsonlist is deprecated. + - feature: DbLog: Added new function : ReadingsVal/ReadingsTimestamp + - feature: Text2Speech: added new attribute TTS_VolumeAdjust + - feature: new module 70_PHTV.pm (loredo) + - feature: JSONMETER: added statistic functions + - feature: LightScene: added scene editor from UliM + - feature: SYSMON: New method: SYSMON_ShowValuesText + - feature: configDB.pm use sql database instead of fhem.cfg (betateilchen) + - feature: new module 98_geodata collect location based data (betateilchen) + - feature: 98_pilight: Added support for Elso protocol + - feature: readingsGroup: added sortDevices attribute + - feature: ENIGMA2: new reading 'recordings', new command record + - change: ENIGMA2: rewrite for NonBlocking + - feature: SYSMOM: new Plot + Doc + - feature: Dashboard: Custom CSS Attribute. Max. 7 Tabs. + - bugfix: Dashboard: Change Groupcontent sorting. Fix Bug that affect + new Groups. + - feature: 10_EnOcean: UTE protocol implemented + - feature: 00_TCM: new command teach + - bugfix: SYSMOM: uninitialized value warning on FritzBox + - added: 09_CUL_FHTTK.pm: german module documentation (matscher) + - feature: readingsGroup: allow FHEMWEB slider and dropdown menus as commands + - feature FB_CALLMONITOR: new attribute "disable" to + disable FB_CALLMONITOR + - feature: YAMAHA_BD: new attribute "disable" to disable cyclic status + updates of player + - change: 09_CUL_FHTTK.pm: added event-on...readings and event-min-interval + updated to reading update mechanism (matscher) + - feature: Dashboard: Groupstitel now can show icons, + Backbutton in Fullsize-Mode + - deleted: 51_BBB_WATCHDOG.pm - not really needed + - bugfix: DbLog: adding ShutdownFunction + - feature: YAMAHA_AVR: new attribute "disable" to disable cyclic status + updates of receiver + - feature: LightScene: added attribute switchingOrder + - added: new module 00_THZ.pm (immiimmi) + - added: new module 98_HTTPMOD.pm (stefanstrobel) + - added: new module 51_BBB_WATCHDOG.pm (betateilchen) + - bugfix: SYSMON: Fix: uninitialized variable + - feature: new modul 73_MPD added (Wzut) + - bugfix: SYSMON: Fix: root fs with /dev/mapper + - feature: Dashboard: The display of the dashboard can be limited to a + defined FHEMWEB. Change view of readingroups + - feature: new module 70_JSONMETER to read obis compatible data in json + format from so called smart meters for electricity, gas or heat + - feature: new modules 10_RESIDENTS, 20_ROOMMATE and 20_GUEST added (loredo) + - feature: LUXTRONIK2: attribute 'doStatistics' calculates boiler gradients + - feature: GEOFANCY: support both apps, Geofency.app and Geofancy.app + - feature: LightScene: added attribute lightSceneRestoreOnlyIfChanged + - bugfix: SYSMON: Fix: CPUTemp & BogoMIPS for utilite-Box. + - bugfix: PRESENCE: fix present-check-interval to be equal with normal + check-interval if not set in define statement and not 30 sec. + - feature: DASHBOARD: Tabs can show an icon. + - bugfix: DASHBOARD: dashboard_showfullsize only in DashboardRoom. + Fix showhelper Bug on lock/unlock. The error that after a trigger + action the curren tab is changed to the "old" + activetab tab has been fixed. + - bugfix: SYSMON: Filesystems (absent medium) + - feature: FLOORPLAN-menu-items can get icons attached by new + attribute fp_roomIcons + - feature: FLOORPLAN-specific icons can now be assigned by just + creating a folder under fhem/images with the flooplan-name + - feature: DASHBOARD: Tabs can set on top, bottom or hidden. + - bugfix: SYSMON: another format for ifconfig output + - feature: DASHBOARD: Use longpoll to update content. + rowcentercolwidth can now be defined per column. + Dashboard can hide FHEMWEB Roomliste and Header => + Fullsizemode. + - bugfix: SYSMON: null reading for absent mount points + - feature: DbLog: jokers "%" in device/reading definition are now possible + - feature: SYSMON: new CPU Statistics and Plots + - feature: changed 10_OWServer.pm and 11_OWDevice.pm to use + NOTIFYDEV (justme1968) + - feature: LightScene: added setcmd command + - feature: DASHBOARD: Dashboard get Tabs. Redesign saving of Group + positioning. + - bugfix: SYSMON: Log Warnings, unnoetige Readings erkenen und entfernen + - feature: LUXTRONIK2: Setting of controller parameter and internal clock + - feature: new module 71_YAMAHA_BD.pm to control Yamaha Blu-Ray + players over network. + - bugfix: DbLog: fix for plotfork + - bugfix: SYSMON: filesystems may be wrong on some systems + - feature: new module 98_pilight.pm added (andreas-fey) + - change: LUXTRONIK2 - made compatible with current developer guidelines + (Blocking.pm, reading update mechanism) + - feature: readingsGroup: added icons and links/commands + - feature: new module 98_Text2Speech.pm added (Tobias Faust) + Google Translator Engine or ESpeak can be used + - feature: YAMAHA_AVR: define separate on and off status intervals for + cyclic status updates + - feature: Visualizations (Plots) for SYSMON added + - feature: new module 42_SYSMON.pm added (hexenmeister) + - feature: YAMAHA_AVR: new readings for radio stations, current title + and more. see commandref for more details. + - feature: new module 32_withings.pm added (justme1968) + - bugfix: PRESENCE: fixing user detection on FritzBox! + - feature: new module 38_CO20.pm added (justme1968) + - feature: new module 98_GEOFANCY.pm added (loredo) + - feature: new module 70_XBMC.pm added (dbokermann) + - feature: new module 51_RPI_GPIO.pm added (klausw) + - bugfix: Dashboard: fixed bug identification an existing Weblink. + fixed bug dashboard_sorting check. Buttonbar can now placed on + top or bottom of the Dashboard. Dashboard is always edited out + the Room Dashboard. + - bugfix: VIERA: fixed bug related to set command remoteControl + - bugfix: ENIGMA2: improved compatibility for Fritzbox and old + Webif versions + - feature: readingsGroup: process events only if visible in browser, + allow
for line breaks in multi-reading lines + - feature: FLOORPLAN: Style4 (S300TH specific) now keeps its formatting + even with longpoll; Text "desiredTemperature" will now + be eliminated - for MAX devices. + - feature: HCS has now MAX Thermostat support + - change: integrated OWServer/OWDevice nonblocking and random start + time patches (justme1968 & Boris) + - feature: Add new module Dashboard + - change: ONKYO_AVR: transfer command database into separate packet + ONKYOdb.pm + - feature: ENIGMA2: bouquet support e.g. for named channels + - feature: Add new module ONKYO_AVR + - feature: SYSSTAT: allow (remote) monitoring via snmp, support + for monitoring windows systems and synology system temperature + - feature: New module LINDY_HDMI_SWITCH.pm added + - change: ENIGMA2: improved logging, default attributes for webCmd and + devStateIcon + - feature: ENIGMA2: support for option channels + - feature: mailcheck: decode non ascii subjet to utf-8, verify gpg signatures + - feature: PRESENCE: "statusRequest" command for lan-bluetooth mode + (collectord >= 1.4, presenced >= 1.1 required) + - feature: PRESENCE: new collectord package + - feature: devspec: removed range, added :FILTER and more general search + - feature: HUEBridge,HUEDevice: support for groups added + - feature: YAMAHA_AVR: new argument "toggle" for mute command + - feature: FB_CALLMONITOR: replace & to & at reverse search + - feature: new module 33_readingsProxy to make (a subset of) a reading + from one device available as a new device. can be used to + separate channels from 1-wire, EnOcean or SWAP multichannel + devices (by justme1968) + - change: improvements for OWDevice and OWServer (justme1968) + - feature: new attribute resolution for 1-wire temperature readings + (justme1968 & Boris) + - feature: new layout commands moveto, moveby and relative positioning + in 02_RSS.pm (Betateilchen & Boris) + - feature: FHEMWEB column attribute + - feature: new layout commands halign, valign, condition in 02_RSS.pm + (Betateilchen & Boris) + - bugfix: PRESENCE: Fix nonworking initialization in mode "lan-bluetooth" + - bugfix: fhem.pl: write-select to avoid blocking in inform/Event Monitor + - bugfix: fix issue with DST changes in 57_Calendar.pm + - feature: new module 36_LaCrosse.pm for LaCrosse IT+ temperature and + humidity sensors with a JeeLabs JeeLink as RF modem. + The matching JeeNode sketch can be found in + .../36_LaCrosse-LaCrosseITPlusReader.zip (by ohweh&justme1968) + - feature: YAMAHA_AVR: new attribute request-timeout. + - bugfix: YAMAHA_AVR: fix missing greater-than sign. Use different + Control-Tag name for RX-Vx75 series + - bugfix: PRESENCE: fixing not working re-initialization when + disabled attribute is set to 0 in lan-bluetooth mode + - feature: LightScene: added attribute lightSceneParamsToSave for + device specific configuration of config" + - feature: readings type added to weblink (justme1968) + - feature: offset and monotonic added to userReadings modifier (justme1968) + - feature: HUEDevice: support SVG icons for LWB001 living whites bulb + - feature: HUEDevice: support more than one bridge + - feature: updateInBackground global attribute + - feature: SYSSTAT: allow stateFormat + - feature: Module 70_VIERA supports now module 95_remotecontrol with own + layout for VIERA TV + - feature: InternalVal function added (like ReadingsVal) + - feature: new module speedtest to monitor internet connection speed with + speedtest-cli + - feature: new module "remotecontrol" to display a graphical remotecontrol + for any device + - feature: HUEDevice: new attribute color-icons to colored svg icons + - feature: FHEMWEB: longpoll is default now, longpollSVG (default off) added + - feature: HUEDevice: allow usage of openautomation svg icons + - feature: FHEMWEB: svg icons / iconPath / www/images/openautomation added + - feature: FHEMWEB: SVGcache attribute & clearSvgCache set command added + - feature: SYSSTAT: allow (remote) monitoring raspberry pi on cpu frequency + - feature: MANTAINER.txt added + - feature: PRESENCE: new mode "shellscript" to use own + scripts or binaries for presence recognition + - feature: YAMAHA_AVR: new set command to select scenes + - feature: PRESENCE: new attribute ping_count + - feature: userReadings may have a filter + - feature: HUEBridge: allow starting of bridge firmware update + - change: EnOcean: profile PM101 changed, old profiles FAH, FBH, FTF, SR04 + removed + - feature: TCM: new attr blockSenderID: + Block receiving telegrams with a TCM SenderID sent by repeaters + - feature: TCM: For TCM120 Transceiver now the transmission of RPS and 4BS + commands supported + - feature: EnOcean: Now all RPS / 1BS profiles, more than 90 4BS profiles and + some manufacturer specific profiles are supported + - feature: EnOcean: profiles (subType) are updated from EEP 2.1 to EEP 2.5 + - feature: FHEMWEB attribute roomIcons added + - feature: SYSSTAT: optionaly calculate geometric average of last 4 + temperature values + - feature: weblink details screen can be used to edit .gplot files + - feature: eventTypes module added, to help with FileLog details screen + - feature: FB_CALLMONITOR: new reverse search provider dasschnelle.at for + reverse search of austrian telephone numbers + - bugfix: event-on-change-reading in combination with event-change-interval + - change: HUEDevice: allow color preset buttons in webCmd + - feature: SYSSTAT: allow (remote) monitoring raspberry pi on chip temperature + - feature: HUEDevice: use webCmdFn for colorpicker + added jscolor for colorpicker + - feature: FHEMWEB: module specific summaryFn/detailFn + defineable webCmdFn + - change: ESA2000: adapted device detection , rename readings + - change: stucture triggers on each change, see event-on-change-reading + - feature: PRESENCE: new mode "function" to use own perl functions for + presence checks + - bugfix: fixing not-working FHEM restart, when a PRESENCE check is running + - bugfix: fixing memory overflow when "list" a PRESENCE definition + - bugfix: fixing dead PRESENCE definitions in case of timeouts + - bugfix: update: error while updating single files fixed. (M. Fischer) + +- 2013-04-08 (5.4) + - feature: updatefhem will be silently converted to update + - feature: FHEMWEB: save button replaced with the menu entry "Save config" + - feature: notify supports $NAME/$EVENT/$EVTPART0/etc. @/% is deprecated. + - feature: 93_DbLog extended to give more functions for the charting frontend. + This includes new queries for raw table data and also statistics, + which get sum/max/min/avg values from the database. + Documentation has been updated. + - feature: new module 31_LightScene to save and restore the state of a + group of lights and other actors + - feature: VIERA module added (by teevau) + - change: FHEMWEB: the first webCmd argument is no longer used by the + state-icon, this can be implemented by the new devStateIcon + - change: 30_HUEDevice: allow autodetection of bridge with hue portal + services + - feature: THRESHOLD Module by Damian + - change: 30_HUEDevice: use new devStateIcon feature to show device color + in room overview + - feature: added example Setup SQL and configuration for SQLite + - change: modified MySQL Setup SQL to use 512 characters in EVENT column + - feature: added new Javascript Frontend based on ExtJS (by Johannes) + - feature: new modules 30_HUEBridge and 31_HUEDevice for phillips hue and + smartlink devices (by justme1968) + - change: SYSSTAT: allow remote monitoring by ssh + - change: SYSSTAT: allow less frequent updates for diskusage + - feature: new module 32_SYSSTAT to monitor system load and disk usage + on linux FHEM hosts (by justme1968) + - feature: new Module 73_PRESENCE to make automatic presence detection of + mobile phones or other mobile devices (like tablets) via ping or + bluetooth checks (by M. Bloch) + - feature: new Module 98_Heating_Control to switch heatsinks automaticly + with a weekly profile (by D. Ortmann / T. Faust) + - feature: new Module 93_DbLog.pm for logging events into Databases. + Generating Plots with weblinks are supportet. + (by B. Neubert / T. Faust) + - feature: new Module 59_HCS.pm for monitoring heating valves (FHT, HM-CC-VD) + to contral a central heating unit. I thank Benjamin for his + support! (M. Fischer) + - feature: new Module 72_FB_CALLMONITOR for receiving telephone call events + (Markus) + - feature: new Module 71_YAMAHA_AVR.pm for controlling Yamaha AV receivers + over network (by Markus) + - feature: optional second parameter to fhem() to make it silent + - feature: autoloading commands, XmlList/etc renamed from 99 to 98. + - feature: FHEMWEB returns external files in chunks to save memory + - feature: commandref.html splitted: documentation is now appended to the + modules. + - change: introduced readingsBulkUpdate, readingsSingleUpdate + - change: added GPLv2 licensing information + - feature: FLOORPLAN added fp_setbutton attribute + - bugfix: FHEMWEB slider with min > 0 + - change: FHEMWEB CORS moved to options + - change: FHEMWEB closing old TCP connections + - change: FHEMWEB added "Associated with" to detail-screen (Uli) + - change: FHEMWEB added ETag headers (Matthias) + - change: FHEMWEB devStateIcon added + - change: HOWTO auf deutsch (ilmtuelp0815) + - change: 98_update.pm due a (probable) bug in perl, modules are no longer + loading automatically. A restart is required now! (M. Fischer) + - feature: 98_update.pm saves the statefile before an update (M. Fischer) + - feature: FHEMWEB longpoll reconnect (Matthias) + - bugfix: rename may overwrite other devices + - feature: FLOORPLAN longpoll (Matthias) + - feature: support for recurring events added in 57_Calendar.pm (Boris) + - feature: added support for OWL CM119,CM160 and CM180, energy sensors in + TRX_WEATHER using RFXtrx433 (Willi Herzig) + - feature: added support for KD101 smoke sensor (also set alert and pair) in + TRX_SECURITY using RFXtrx433 (Willi Herzig) + - change: changed dewpoint to work with event-on-change-reading and + technoline TX3TH (Willi Herzig) + - feature: new command fheminfo. Shows system informations. (M. Fischer) + - feature: added support for UV sensors in TRX_LIGHT using RFXtrx433 (Willi + Herzig) + - feature: added on-till and on-timer for set in TRX_LIGHT using RFXtrx433 + (Willi Herzig) + - feature: generate devices with hexcodes as state for unknown types in + TRX_ELSE using RFXtrx433 (Willi Herzig) + - feature: new modules 10_OWServer.pm and 11_OWDevice.pm to interface with + OWFS + - feature: stateFormat (readingsFn modules) and showInternalValues attributes + - feature: new readingsFn modules: FS20 CUL_WS HMS CUL_EM CUL_TX EnOcean ZWave + - change: BS, USF1000, ECMDDevice, Weather, dummy migrated to readingsFN + (Boris) + - feature: telnet client mode + - bugfix: FHEMWEB longpoll misses initial state change (HM: set_on vs. on) + - change: 20_OWFS.pm, 21_OWTEMP modules flagged as "deprecated". These + modules will be removed in a future release. Use OWServer / + OWDevice instead. (M. Fischer) + - feature: a lot of new features and known 1-wire slaves to OWServer / + OWDevice added (M. Fischer) + - feature: set-extensions (additional set commands) for FS20, EnOcean, ZWave + - feature: added new command 'notice'. (M. Fischer) + - change: update supports the display and confirmation of system messages + via the new notice command (M. Fischer) + - feature: added new set commands and basicauth to 49_IPCAM.pm (M. Fischer) + - feature: userReadings + - feature: average supports more than one value in combined readings (T:x H:y) + - feature: FHEMWEB serves arbitrary files from the www directory + - feature: FB_checkPw now works with a distinct fritzbox user + - bugfix: floorplan-correction for readings with longpoll. Requires local + change in css! + - feature: floorplan added js-extension from Dirk + - feature: hour resolution in SVG + - feature: ZWave support for MULTI_CHANNEL class + - feature: FHEMWEB: old-dir-support removed, image-indexing rebuilt, + smallscreen/touchpad moved to stylesheetPrefix, menuEntries + added, Extend devStateIcon, js setting of attr values in detail + screen, live slider update in detail and room view + - feature: added support for third-party packages to 98_update.pm (M. Fischer) + - feature: FBAHA/FBDECT for FRITZ!DECT devices + - feature: event-min-interval Attribute + +- 2012-10-28 (5.3) + - feature: added functions trim, ltrim, rtrim, UntoggleDirect, + UntoggleIndirect + - feature: added functions FB_mail, FB_WLANswitch + - rework: CUL_HM reworks with respect to protocol. additions for several + devices and commands + - feature: rfmode supports to listen to MAX if fw>1.46, 00_CUL.pm (Jens) + - feature: Status and length on cmdStack in webinterface for 10_CUL_HM + - feature: devicepair in 10_CUL_HM.pm supports unset + - feature: devicepair for single Button in 10_CUL_HM.pm (by MartinP) + - feature: new Modules 75_MSG.pm, 76_MSGFile.pm and 76_MSGMail.pm (by + Ruediger) + - feature: new Module 59_Twilight.pm to calculate current daylight + - feature: internal NotifyOrderPrefix: 98_average.pm is more straightforward + - feature: the usb command tries to flash unflashed CULs on linux + - feature: FHEMWEB: jsonp support, .holiday and .cfg added to Edit Files + - feature: SVG: filled area support, some ls/lw fixes + - feature: WOL (wake on lan) module added (by Matthias) + - feature: additional groups from /etc/groups are applied (Christopher) + - feature: updatefhem backup is using tar+gzip now + - feature: EIB: introduce Get, interpret received values upon defined model + (by datapoint types) (Maz) + - feature: NetIO230B module by Andy + - feature: Retaining configfile comments (not within a define statement) + - feature: EnOcean PM101 by Ignaz + - feature: FHEMWEB redirectCmds attribute added + - feature: CUL_TX minsecs attribute (by Arno) + - feature: webCmd in smallScreen added + - feature: TRX modules by Willi + - feature: FHEMWEB icons (by Joerg) + - feature: FHEMWEB console (same as inform timer) + - feature: remove dependency on Google::Weather, major rewrite (Boris) + - feature: started experimental interface implementation (fhem API v2) + (Boris) + - feature: sleep issued in at/notify/etc is not blocking fhem anymore + - feature: dummy got a setList attribute + - feature: new module 02_RSS.pm + - feature: at attribute alignTime added + - feature: FHEMWEB attribute values via dropdown, slider for dimmer + - feature: new attribute group for FHEMWEB (Boris) + - change: 11_FHT.pm, 50_WS300.pm, 59_Weather.pm migrated to readingsUpdate + mechanism (Boris) + - change: 59_Weather.pm migrated from Google to Yahoo Weather API (Boris) + - change: updatefhem modifications to support a clean install of fhem and + pgm2 installation, see commandref.html (M. Fischer) + - change: FHEMWEB support for the new www/pgm2 directroy added (M. Fischer) + - change: Makefile support for for the new www/pgm2 directroy and new + targets backup and uninstall added. More verbose output. (M. Fischer) + - change: backup separated from updatefhem to a new command (M. Fischer) + - feature: new command backup added (M. Fischer) new global attribute + added new global attribute added new global + attribute added + - feature: new module 57_Calendar.pm (Boris) + - feature: new parameter for updatefhem added (M. Fischer) new + global attribute added (M. Fischer) + - feature: optional telnet password added / telnet port is optional + - feature: holiday returns all matches, not only the first. + - change: CULflash separated from updatefhem to a new module (M. Fischer) + - feature: time and internet helper routines added to fhem.pl (Boris) + - change: separating common functions used by the FHEM modules into + *Utils.pm files from fhem.pl + - feature: portpassword and basicAuth may use evaluated functions + - feature: motd with SecurityCheck added + - feature: telnet module added, attr global port moved. allowfrom changed. + - feature: FhemUtils/release.pm for the new update process added. (M. + Fischer) + - bugfix: correct one-time relative at commands after reboot + - feature: ZWave added + - feature: module IPCAM added. (M. Fischer) + - feature: module HTTPSRV added (Boris) + - feature: module FLOORPLAN added (Uli Maass) + - bugfix: FHEMWEB: weblink with group attribute is shown together with other + elements + - feature: FHEMWEB: timepicker added + - feature: FHEMWEB: support for modul specific icons added (M. Fischer) + + +- 2011-12-31 (5.2) + - bugfix: applying smallscreen attributes to firefox/opera + - feature: CUL_TX added (thanks to Peterp) + - feature: TCM120/TCM310 + EnOcean parser added + - feature: eventMap enhanced + - bugfix: enabled logging for 59_Weather.pm (Boris) + - feature: language selection for 59_Weather.pm (Erwin) + - feature: .gplot files renamed from type to content + - bugfix: FS20 on-for-timer error reporting only in the logfile + - bugfix: FHEM2FHEM should work with CUL again, after syntax change + - feature: CUL directio mode (No Device::SerialPort needed) + - feature: FritzBox 7270 ZIP file + - bugfix: prevent fhem from stalling if telnet times out in 66_ECMD.pm + - feature: added postproc ability to classdef in 66_ECMD.pm (Boris, Heinz) + - feature: FHEMWEB longpoll mode, small fixes, tuned smallscreen mode + - feature: average module added + - change: moved the berliOS CVS repository to a sourceforge SVN repository + - feature: all FHEM modules have now a subversion id. + - bugfix: new perl compiled for the FritzBox 7270 + - feature: regexp1WontReactivate Attribute added + - bugfix: XmlList special handling + - bugfix: CUL_WS rain sensor corr1 fix + - feature: FHEMWEB stylesheet attribute repaced with stylesheetPrefix + - feature: notify attribute forwardReturnValue + - change: move JsonList from contrib to main-modules + - change: JsonList output optimized and more structured + - feature: FHEMWEB save button, smallscreen first screen fix + - feature: FHEMWEB encoding is now UTF-8, alias attribute is respected + - change: HTTPS certs directory moved from cwd into modpath + - feature: shutdown parameter restart added + - feature: usb scan/create command added (part of autocreate). + - feature: SaveAs added to FHEMWEB Edit-Files + - feature: EnOcean ElTako dimmer by Marc. + - feature: fhem is started as user fhem on the FB7390 + + +- 2011-07-08 (5.1) + - feature: smallscreen optimizations for iPhone + - feature: FHT8V rewrite (and moved from contrib into the FHEM directory). + - feature: PID rewrite (and moved from contrib into the FHEM directory). + - feature: FHEM2FHEM module + - bugfix: CUL get should not digest foreign events (fhtsoftbuffer) + - bugfix: S300TH sanity check won't allow negative temperatures. + - feature: decode CUL uptime + - feature: USB doc changes, FHZ initFS20_02/stopHMS parameters by Andreas. + - feature: CUL_HM for some HomeMatic devices. + - bugfix: HTML-Syntax check of the pgm2 output and documents (*.html) + - feature: added date alias for FHT80b (Boris) + - feature: attr may be a regexp (for CUL_IR) + - feature: Homepage moved from koeniglich.de/fhem to fhem.de + - feature: eventMap attribute + - feature: 64_ESA2000 added (by STefan/Gerd) + - feature: new modules 66_ECMD.pm and 67_ECMDDevice.pm for ethersex-enabled + devices and alike. + - bugfix: serial port setting on Linux broken if running in the background + - feature: IPV6 support, FHEMWEB basicAuth and HTTPS support + - feature: createlog added to the autocreate module + - feature: contrib/tcptee.pl added + - feature: HMLAN support + - feature: Fritzbox7390 image + - feature: pgm2 tablet support, included into the default configuration + - feature: TUL/EIB Support (by Maz) + - feature: updatefhem/CULflash + - feature: $value{} => Value(), $oldvalue => OldValue()/OldTimestamp() + + +- 2010-08-15 (5.0) + - **NOTE*: The default installation path is changed to satisfy lintian + - feature: KM271 + - bugfix: 99_SUNRISE_EL endless loop bug + - feature: CUL: optional baudrate spec in definition + - feature: CUL: sendpool attribute + - feature: CUL_HOERMANN module added + - bugfix: DST change: absolute at and relative sunrise fix + - feature: FHEMWEB javascript additions for SVG plots (click on lines/labels) + - feature: FHEMWEB smallscreen attribute (for smartphones) + - bugfix: the internal fhem() used in perl oneliners does not return a value + - feature: Dimmer function of X10 module changed to match FS20 + - feature: allow only meaningful readings (fill level > -5%) in USF1000 + - feature: device attr links in commandref.html + - bugfix: make BS known to CUL to avoid lost messages if both FHZ1300 and CUL + are connected, adjust matching rule + - feature: Copy&Paste in SVG + - feature: Debian/Ubuntu Package. Install-path changes to satisfy lintian + - feature: Allnet 3076/4027/4000T + - feature: RFXCOMM Module for Oregon Weatherstations + - feature: Davis VantagePro2 + - feature: ELV USB-WDE1 + - feature: addvaltriggers CUL attribute for adding RSSI as a trigger + - feature: CUL_WS sanity check for large temp differences. + +- 2010-03-13 (4.9) + - bugfix: changed the fhem prompt from FHZ> to fhem> + - bugfix: CUL_RFR fixes (chaining RFR's should work) + - bugfix: Path in the examples fixed (got corrupted) + - bugfix: PachLog fixes from Axel + - bugfix: HOWTO/Examples revisited for correctness + - bugfix: INITIALIZED, DEFINED, RENAMED, DELETED triggers + - feature: image weblinks from Stefan + - feature: OWFS support for passive Devices e.g. DS9097 (see commandref.html) + - bugfix: OWFS crash fhem with PGM2/3, xmllist (M.Fischer) + - bugfix: OWTEMP Defining a device without OWFS now fails (M.Fischer) + - bugfix: 21_OWTEMP.pm missing trigger fo notify/filelog (M.Fischer) + - feature: 99_getstate.pm get state from S555TH now (M.Fischer) + - feature: pgm3: automatic support for CUL_WS (S300TH) added (MartinH) + - bugfix: 21_OWTEMP.pm missing space within state logging (M.Fischer) + - bugfix: 21_OWTEMP.pm interval fixed (M.Fischer) + - bugfix: 21_OWTEMP.pm rewrite with errorcontrol and demo mode (M.Fischer) + - feature: ignore attribute + - bugfix: [pgm3] table-format on Android-Browser optimized + - feature: [pgm3] Skinable - change the colors. + - feature: [pgm3] Rooms possible for Webcam and Google-Weather + - bugfix: dummy/structure was listed twice in list and xmllist + - feature: 11_FHT.pm added new readings for warnings on battery, lowtemp, + window and windowsensor (M.Fischer) + - feature: autocreate.pm (create undefined RF devices, logs and plots) + - feature: on-for-timer added for X10 modules (Boris) + - bugfix: pgm3: Better check of availability of google-weather (MartinH) + - feature: pgm3: DBLog added for everything except UserDefs + (Gerhard Pfeffer / MartinH) + - feature: pgm2 style changes, SVG in background, optional compression + +- 2009-11-28 (4.8) + - bugfix: loosing data when sending FS20 messages in a group + - bugfix: better handling of disconnected CUN + - feature: softfhtbuffer added to CUL + - bugfix: pgm3: Pulldown-Menu FHTDEV with error-check (MartinH) + - feature: duplicate buffer added for multi-cul/-fhz setups + - feature: 20_OWFS.pm for 1-Wire via OWFS added (Martin Fischer) + - feature: 21_OWTEMP.pm for 1-Wire Digital Thermometer added (Martin Fischer) + - feature: CUL_FHTTK from Kai + - feature: pgm3: Google-Weather, Battery-Check, Log-View added (MartinH) + - feature: CUL_RFR (RF_ROUTING) added + - feature: Command save retains now the order of the old config file + - feature: List parameter added (list .* RFR_MSGCNT) + +- 2009-10-23 (4.7) + - bugfix: Reattached corrupted CUL device caused uninitialized message + - bugfix: CUL/HMS changes, HMS cleanup + - bugfix: EM/EMWZ/EMGZ set changed to work in FHEMWEB + - bugfix: Avoid unitialized in xmllist for corrupt readings, reporter Boris + - bugfix: Add binmode to 01_fhemweb.pm for windows + - bugfix: Uniform check for windows, enable CUL for windows. + - bugfix: CUL/HMS parsing patches from Peter + - bugfix: Fixes for Windows by Klaus + - bugfix: Another "rereadcfg" bugfix + - feature: Update to the current (1.27) CUL FHT interface + - feature: suppress inplausible readings from USF1000 + - feature: get time, fwrev, set reopen for CM11 (Boris 2009-09-12) + - bugfix: FHZ_ReadAnswer bugfix for Windows (Klaus, 20.8.2009) + - feature: CUL: device access code reorganized, TCP/IP support added (CUN) + - feature: Pachube module from Axel + - feature: dumpdef module from Axel in contrib + - feature: javascripting support in FHEMWEB (Klaus/Axel) + - feature: Module 09_BS.pm for brightness sensor added (Boris 2009-09-20) + +- 2009-07-03 (4.6) + - bugfix: fht actuator message clarification by Klaus + - feature: getstate command from Martin (25.12) + - bugfix: at drifts for relative timespecs + - bugfix: Add IODev to CUL/EM/CUL_WS/HMS/KS300 + - bugfix: FileLog get (pgm2 plots) wont find the first row in the file + - feature: 00_CUL: Answer CUR requests (status/time/fht) + - bugfix: support for second correction factor for EMWZ in 15_CUL_EM added + - feature: CUL further sets/gets added + - feature: Removed msghist for multiple FHZ handling, IODev attribute added + - bugfix: cut off string "(counter)" from fallback value in 13_KS300.pm + - feature: daily/monthly cumulated values for EMWZ/EMGZ/EMWM with 15_CUL_EM + - feature: 01_FHEMWEB.pm: multiple room assignments + - feature: 01_FHEMWEB.pm: fixedrange with optional [day|week|month|year] + - feature: 01_FHEMWEB.pm: attr title and label for flexible .gplot files + - feature: fhem.pl: attr global logdir used by wildcard %ld + - feature: do not block on disconnected devices (FHZ/CM11/CUL) + - bugfix: deleting at definition in the at command + - bugfix: deleting a notify/at/watchdog definition in a notify/at/watchdog + - feature: devspec =. E.g. set room=kitchen off; list disabled= + - feature: Common Module calling for CUL/FHZ/CM11 + - feature: Store CUL sensitivity info + - feature: avoid the "unknown/help me" message for unloaded devices + - feature: structure module for large installations + - feature: Cost Control in 15_CUL_EM (CostPerUnit, BasisFeePerMonth) + - feature: add counter differential per time in 81_M232Counter.pm + - feature: added USB compendium to documentation + - feature: pgm3: Documentation for pgm3 updated, HMS100CO added (and bugfix) + - bugfix: Defining a repeated at job in a sunrise/sunset at job fails + - bugfix: FHT "summer" fix (avoiding a lot of syncnow) + - feature: FHEMWEB modules added + - feature: holiday module + doc + example + holiday2we attribute + - bugfix: sunrise stuff fixed, doc missing + - feature: CUL FHT sending added + - bugfix: workaround to make M232 counter wraparound + - feature: sequence module added + - feature: Google Weather API support for FHEM (Boris 2009-06-01) + - feature: lazy attribute for FHT devices (Boris 2009-06-09) + - feature: tmpcorr attribute for FHT devices + - feature: CUL_EM generates an event for each of the READINGS + - feature: USF1000S support for FHEM added (Boris 2009-06-20) + - feature: CUL supports HMS (culfw >= 1.22 needed) + - feature: CUL shutdown procedure added + - feature: 14_CUL_WS: better error checking + - bugfix: webpgm2 multi line editing is working again + +- 2008-12-23 (4.5) + - bugfix: further 01_FHEMWEB cleanup + - feature: CUL support for FS20(r/w), FHT(readonly), KS300 and EM + - feature: command list outputs the device attributes too + - bugfix: rename bugs fixed + - bugfix: better integration of ReadyFn (Windows), slight overall speedup + - bugfix: Ignore/correct casing when autoloading modules + - bugfix: at is executed twice after a modify (rufus99, 2008-09-10) + - feature: FHT internal modifications (better protocol understanding) + - feature: add timestamp to inform + - feature: The strange stty settings in 00_FHEM.pm are optional + - bugfix: webpgm2 iPhone fix + - feature: fullinit and reopen commands for FHZ added (Boris 2008-11-01) + - bugfix: undefined NotifyFn in fhem.pl (Boris 2008-11-01) + - feature: new modules 00_CM11.pm and 20_X10.pm for integration of X10 + devices in fhem (Boris 2008-11-02) + - feature: X10 support for pgm3 (Boris 2008-11-02) + - bugfix: FHT short message warning + - bugfix: rereadconfig crashes with active webpgm2 connections (2008-11-13) + - bugfix: watchdog crash (2008-11-15) + - bugfix: Strange call for nonexistent MyCUL: ReadFn + - feature: webpgm2: gplot output goes to /tmp/gnuplot.err + - feature: devspec TYPE,DEF,STATE. e.g. list TYPE:FS20, set DEF:123 on + - bugfix: at schedules 2 events after the DST change (fix not verified) + - feature: commandref.html reorg. There are now device sections. + - feature: CUL / CUL_EM / CUL_WS documentation + - feature: do not block fhem when the CUR is disconnected + - bugfix: correct correction factors for EMEM in 15_CUL_EM.pm + - bugfix: more stable CUL initialization + - feature: reworked 15_CUL_EM.pm to account for timer wraparounds, more + readings added + - feature: speed gain through disabled refreshvalues query to all FHTs at + definition; if you want it back at a "set myFHT report1 255 + report2 255" command to the config file. + - feature: fhem commands may be added in modules. XmlList is external now. + - bugfix: rereadcfg from webpgm2 does not crash fhem.pl + - feature: jsonlist command from Martin (contrib/JsonList) + - feature: contrib/rotateShiftWork from Martin + - feature: contrib/fhem2speech from Martin + - bugfix: attributes of at devices disappear + - feature: attribute rainadjustment for KS300 (Boris 2008-12-17) + - bugfix: deleting at / watchdog while active creates an empty device + - feature: ExactId trigger added for wildcard HMS devices + +- 2008-08-04 (4.4) + - feature: RM100-2 battery empty warning (mare 23.07.08) + - feature: optimising the pgm2/SVG memory usage + - feature: autoloading FHEM modules + - bugfix: STATE/$value is carrying again the correct value + - feature: enhancing the Makefile and the documentation + - feature: 90_at is using now InternalTimer, subsecond precision added + - feature: HMS100-FIT added (01.01.08 by Peter and 22.01.08 by Juergen) + - feature: 91_watchdog added to handle the HMS100-FIT + - feature: cum_kWh/cum_m3 added to EMWZ/EMGZ (11.01.08 by Peter) + +- 2008-07-12 (4.3) + - bugfix: KS300 state was wrong after the STATE bugfix + - feature: HMS100CO (by Peter) + - feature: EMGZ (by Peter) + - feature: Generate warning if too many commands were sent in the last hour + - doc: linux.html: Introduction (Peter S.) + - feature: contrib/82_M232Voltage.pm (by Boris, 24.12) + - feature: delattr renamed to deleteattr (Rudi, 29.12) + - feature: defattr renamed to setdefaultattr (Rudi, 29.12) + - feature: device spec (list/range/regexp) for most commands implemented + - feature: %NAME, %EVENT, %TYPE parameters in notify definition + - feature: added 93_DbLog.pm, database logging facility (Boris, 30.12.) + - feature: webfrontend/pgm2 converted to a FHEM module + - bugfix: 99_SUNRISE_EL.pm: may schedule double events + - bugfix: 62_EMEM.pl, contrib/em1010.pl: correct readings for energy_kWh + and energy_kWh_w (Boris, 06.01.08) + - feature: global attr allowfrom, as wished by Holger (8.1.2008) + - feature: FHT: multiple commands, softbuffer changes, cmd rename, doc + - feature: EM1010PC: automatic reset + - feature: contrib/00_LIRC.pm (25.3, by Bernhard) + - bugfix : 00_FHZ: additional stty settings for strange Linux versions + - bugfix : pgm2 wrong temp summary for FHT's (reported by O.D., 16.4.2008) + - feature: FHEM modules may live on a filesystem with "ignorant" casing (FAT) + - feature: FileLog "set reopen" for manual tweaking of logfiles. + - feature: multiline commands are supported through the command line + - feature: pgm2 installation changes, multiple instances, external css + - feature: 87_ws2000.pm (thomas 10.05.08) + - contrib: ws2000_reader.pl Standalone decoder and server (thomas 10.05.08) + - doc: update fhem.html and commandline.html reflecting ws2000 and + windows installation(thomas 10.05.08) + - feature: add ReadyFn to fhem.pl in main loop to have an alternative for + select, which is not working on windows (thomas 11.05) + - feature: set timeout to 0.2s, if HandleTimeout returns undef=forever + - bugfix : WS2000:fixed serial port access on windows by replacing FD with + ReadyFn + - bugfix : FileLog: dont use FH->sync on windows (not implemented there) + - feature: EM, WS300, FHZ:Add Switch for Device::SerialPort and + Win32::SerialPort to get it running in Windows (sorry, untested) + - bugfix: FileLog undefined $data in FileLog_Get + - feature: fhem.pl check modules for compiletime errors and do not initialize + them + - feature: M232 add windows support (thomas 12.05.08) + - feature: add simple ELV IPWE1 support (thomas 12.05.08) + - feature: FileLog get to read logfiles. Used heavily by webpgm2 + - feature: webpgm2: gnuplot-scroll mode to navigate/zoom in logfiles + - bugfix: deleting FS20 device won't result in unknown device (Daniel, 11.7) + - feature: webpgm2 generates SVG's from logs: no need for gnuplot + - bugfix: examples corrected to work with current syntax + +- 2007-12-02 (4.2) + - feature: added archivedir/archivecmd to the the main logfile + - feature: 99_Sunrise_EL.pm (does not need any Date modules) + - bugfix: seldom xmllist error resulting in corrupt xml (Martin/Peter, 4.9) + - bugfix: FHT mode holiday_short added (9.9, Dirk) + - bugfix: Modifying a device from its own trigger crashes (Klaus, 10.9) + - feature: webpgm2 output reformatted + - feature: webpgm2 displaying multiple plots + - feature: FHT lime-protection code discovered by Dirk (7.10) + - feature: softwarebuffer for FHT devices (Dirk 17.10) + - feature: FHT low temperatur warning and offset (Dirk 17.10) + - change: change FHT state into warnings (Dirk 17.10) + NOTE: you'll get an undefined type state & + undefined type unknown_85 after upgrade. + - feature: Softwarebuffer code simplified (Rudi 22.11) + - bugfix: bug #12327 doppeltes my + - bugfix: set STATE from trigger + - bugfix: readings state vs STATE problem (xmllist/trigger) + - change: SUNRISE doc changed (99_SUNRISE.pm -> 99_SUNRISE_EL.pm) + - feature: support for the M232 ELV device (Boris, 25.11) + - feature: alternativ Quad-based numbers for the FS20 (Matthias, 24.11) + - feature: dummy type added (contrib/99_dummy.pm) + +- 2007-08-05 (4.1) + - doc: linux.html (private udev-rules, not 50-..., ATTRS) + - bugfix: setting devices with "-" in their name did not work + - doc: fhem.pl and commandref.html (notifyon -> notify, correction + of examples) + - feature: modify command added + - feature: The "-" in the name is not allowed any more + - bugfix: disabled notify causes "uninitialized value" (STefan, 1.5) + - bugfix: deleted FS20 items are still logging (zombie) (Gerhard, 16.5) + - bugfix: added FS20S8, removed stty_parmrk (Martin, 24.5) + - feature: added archivedir/archivecmd to the FileLog + - feature: added EM1010PC/EM1000WZ/EM1000EM support + - bugfix: undefined messages for unknown HMS devs (Peter, 8.6) + - bugfix: em1010 and %oldvalue bugs (Peter, 9.6) + - bugfix: SCIVT solar controller (peterp, 1.7) + - bugfix: WS300 loglevel change (from 2 to 5 or device specific loglevel) + - feature: First steps for a Fritz!Box port. See the fritzbox.html + +- 2007-04-14 (4.0) + - bugfix: deny at +{3}... (only +*{3} allowed), reported by Bernd, 25.01 + - bugfix: allow numbers greater then 9 in at +{} + - feature: new 50_WS300.pm from Martin (bugfix + rain statistics, 26.01) + - feature: renamed fhz1000 to fhem + - feature: added HISTORY and README.DEV + - doc: Added description of attribute "model". + - bugfix: delete the pidfile when terminating. (reported by Martin and Peter) + - feature: attribute showtime in web-pgm2 (show time instead of state) + - feature: defattr (default attribute for following defines) + - feature: added em1010.pl to the contrib directory + - doc: added linux.html (multiple devices, udev-links) + - REORGANIZATION: + - at/notify "renamed" to "define at/notify" + - logfile/modpath/pidfile/port/verbose "renamed" to "attr global xxx" + - savefile renamed to "attr global statefile" + - save command added, it writes the configfile and the statefile + - delattr added + - list/xmllist format changed + - disable attribute for at/notify/filelog + See HISTORY for details and reasoning + - added rename command + - webpgm2 adapted to the new syntax, added device specific attribute + and "set" support, gnuplot files are configurable, links to the + documentation added. + - bugfix: more thorough serial line initialization + +- 2007-01-25 (3.3) + - bugfix: 50_WS300.pm fix from Martin + - bugfix: pidfile does not work as expected (reported by Martin) + - bugfix: %U in the log-filename is wrong (bugreport by Juergen) + - feature: %V added to the log-filename + - feature: KS300 wind calibration possibility added + - feature: (software) filtering repeater messages (suggested by Martin) + - feature: the "client" fhz1000.pl can address another host + - bugfix: empty FHT battery is not reported (by Holger) + - feature: new FHT codes, e.g. month/day/hour/minute setting (by Holger) + +- 2007-01-14 (3.2) + - bugfix: example $state changed to $value (remco) + - bugfix: sun*_rel does not work correctly with offset (Sebastian) + - feature: new HMS100TF codes (Sebastian) + - feature: logging unknown HMS with both unique and class ID (Sebastian) + - feature: WS300: "Wetter-Willi-Status", rain_raw/rain_cum added, historic + data (changes by Martin & Markus) + - bugfix: broken rereadcfg / CommandChain after init + (reported by Sebastian and Peter) + - bugfix: sunrise_coord returned "3", which is irritating + +- 2007-01-08 (3.1) + - bugfix: delete checks the arg first "exactly", then as a regexp + - bugfix: sun*_rel does not work correctly with offset (Martin) + - feature: FAQ entry on how to install the sunrise stuff. + - feature: the inner core is modified to be able to handle + more than one "IO" device, i.e multiple FHZ at the same time, + or FHZ + FS10 + WS300. Consequences: + - "fhzdev " replaced with "define FHZ " + - "sendraw " replaced with "set raw " + - module function parameters changed (for module developers) + - set FHZ activefor dev + - select instead sleep after sending FHZ commands + - the at timer is more exact (around 1msec instead of 1 sec) + - ignoring FS20 device 0001/00/00 + - feature: contrib/serial.pm to debug serial devices. + - feature: WS300 integrated: no external program needed (Martin) + - feature: updated to pgm3-0.7.0, see the CHANGELOG at Martins site + +- 2006-12-28 (3.0) + - bugfix: KS300: Make the temperature negative, not the humidity + - bugfix: generate correct xmllist even with fhzdev none (Martin, 12.12) + - feature: one set command can handle multiple devices (range and enumeration) + - feature: new FS20 command on-till + - feature: perl: the current state is stored in the %value hash + - feature: perl: sunset renamed to sunset_rel, sunset_abs added (for on-till) + - feature: perl: isday function added + - feature: follow-on-for-timer attribute added to set the state to off + - bugfix: the ws300pc negative-temp bugfix included (from Martin Klerx) + - feature: version 0.6.2 of the webpgm3 included (from Martin Haas) + +- 2006-11-27 (2.9a) + - bugfix: FileLog+Unknown device generates undefined messages + - bugfix: trigger with unknown device generates undefined messages + +- 2006-11-19 (2.9) + - bugfix: fhz1000.pl dies at startup if the savefile does not exist + - bugfix: oldvalue hash is not initialized at startup (peter, Nov 09) + - feature: Notify reorganization (requested by juergen and matthias) : + - inform will be notified on both real events and set or trigger commands + - filelogs will additionally be notified on set or trigger commands + - the extra_notify flag is gone: it is default now, there is a + do_not_notify flag for the opposite behaviour. + - feature: at timespec as a function. Example: at +*{sunset()} + commandref.html and examples revisited. + - feature: 99_SUNRISE.pm added to use with the new at functionality + (replaces the old 99_SUNSET.pm) + - feature: webpgm2 "everything" room, at/notify section, arbitrary command + - bugfix: resetting the KS300 + - feature: updated ws300pc (from martin klerx, Nov 08) + - bugfix: parsing timed commands implemented => thermo-off,thermo-on and + activate replaced with timed off-for-timer,on-for-timer and + on-old-for-timer (reported by martin klerx, Nov 08) + - feature: pidfile (requested by peter, Nov 10) + - bugfix: function 81 is not allowed + +- 2006-11-08 (2.8) + - feature: store oldvalue for triggers. perl only. requested by peter. + - feature: inform cmd. Patch by Martin. There are many Martins around here :-) + - bugfix: XML: fix & and < and co + - bugfix: Accept KS300 negative temperature values + - change: the FS20 msg "rain-msg" is called now "activate" + - feature: start/stop rc script from Stefan (in the contrib directory) + - feature: attribute extra_notify: setting the device will trigger a notify + - feature: optional repeat count for the at command + - feature: skip_next attribute for the at command + - feature: WS300 support by Martin. Check the contrib/ws300 directory. + - bugfix: 91_DbLog.pm: retry if the connection is broken by Peter + - feature: Martin's pgm3-0.5.2 (see the CHANGELOG on his webpage) + - feature: RRD logging example by Peter (in the contrib/rrd directory) + +- 2006-10-03 (2.7) + - bugfix: Another try on the > 25.5 problem. (Peters suggestion) + - feature: 99_ALARM.pm from Martin (in the contrib directory) + - feature: HMS100TFK von Peter P. + - feature: attribute loglevel + - feature: attribute dummy + - feature: attr command documented + - feature: the current version (0.5a) of the pgm3 from Martin. + +- 2006-09-13 (2.6a) + - bugfix: the FHT > 25.5 problem again. A never ending story. + +- 2006-09-08 (2.6) + - bugfix: updated the examples (hint from Juergen) + - bugfix: leading and trailing whitespaces in commands are ignored now + - feature: making life easier for perl oneliners: see commandref.html + (motivated by STefans suggestions) + - feature: include command and multiline commands in the configfiles (\) + - bugfix: web/pgm2 KS300 rain plot knows about the avg data + - bugfix: the FHT > 25.5 problem. Needs to be tested. + - feature: log unknown devices (peters idea, see notifyon description) + - feature: HMS wildcard device id for all HMS devices. See the define/HMS + section in the commandref.html for details. + NOTE: the wildcard for RM100-2 changed from 1001 to 1003. + (peters idea) + - feature: rolwzo_no_off.sh contrib file (for those who were already closed + out by automatically closing rollades, by Martin) + - feature: the current version (0.4.5) of the pgm3 from Martin. + +- 2006-08-13 (2.5) + Special thanks to STefan Mayer for a lot of suggestions and bug reports + - If a command is enclosed in {}, then it will be evaluated as a perl + expression, if it is enclosed in "" then it is a shell command, else it is + a "normal" fhz1000 command. + "at" and "notifyon" follow this convention too. + Note: a shell command will always be issued in the background. + - won't die anymore if the at spec contains an unknown command + - rereadcfg added. Sending a HUP should work better now + - escaping % and @ in the notify argument is now possible with %% or @@ + - new command trigger to test notify commands + - where you could specify an fhz command, now you can specify a list of + them, separated by ";". Escape is ;; + - KS300 sometimes reports "negative" rain when it begins to rain. Filter + such values. israining is set when the raincounter changed or the ks300 + israining bit is set. + - sleep command, with millisecond accuracy + - HMS 100MG support by Peter Stark. + - Making FHT and FS20 messages more uniform + - contrib/fs20_holidays.sh by STefan Mayer + (simulate presence while on holiday) + - webfrontends/pgm4 by STefan Mayer: fs20.php + - KS300 avg. monthly values fixed (hopefully) + - deleted undocumented "exec" function (you can write it now as {...}) + +- 2006-07-23 (2.4) + - contrib/four2hex (to convert between ELV and our codes) by Peter Stark + - make dist added to set version (it won't work in a released version) + - reload function to reload (private) perl modules + - 20_FHT.pm fix: undef occures once without old data + - "setstate comment" is replaced with the attr command (i.e. attribute). + The corresponding xmllist COMMENT tag is replaced with the ATTR tag. + Devices or logs can have attr definitions. + - webfrontend/pgm2 (fhzweb.pl) updated to handle "room" attributes(showing + only devices in this room). + - version 0.4.2 of webfrontend/pgm3 integrated. + - contrib/ks300avg.pl to compute daily and monthly avarage values. + - the 40_KS300.pm module is computing daily and monthly avarages for the + temp/hum and wind values and sum of the rain. The cum_day and cum_month + state variables are used as helper values. To log the avarage use the + .*avg.* regexp. The regexp for the intraday log will trigger it also. + - Added the contrib file garden.pl as a more complex example: garden + irrigation. The program computes the time for irrigation from the avarage + temperature reported by the ks300-2. + - Enable uppercase hex codes (Bug reported by STefan Mayer) + - Renamed the unknown_XX FHT80b codes to code_XXXXXX, this will produce + "Undefined type" messages when reading the old save file + - RM100-2 added (thanks for the codes from andikt). + +- 2006-6-22 (2.3) + - CRC checking (i.e. ignoring messages with bad CRC, message on verbose 4) + - contrib/checkmesg.pl added to check message consistency (debugging) + - FHT: unknown_aa, unknown_ba codes added. What they are for? + - Empty modpath / no modpath error messages added (some user think modpath is + superfluous) + - Unparsed messages (verbose 5) now printed as hex + - Try to reattach to the usb device if it disappears: no need to + restart the server if the device is pulled out from the USB socket and + plugged in again (old versions go into a busy loop here). + - Supressing the seldom (ca 1 out of 700) short KS300 messages. + (not sure how to interpret them) + - Added KS300 "israining" status flag. Note: this not always triggers when it + is raining, but there seems to be a correlation. To be evaluated in more + detail. + - notifyon can now execute "private" perl code as well (updated + commandref.html, added the file example/99_PRIV.pm) + - another "perl code" example is logging the data into the database + (with DBI), see the file contrib/91_DbLog.pm. Tested with an Oracle DB. + - logs added to the xmllist + - FHT80b: Fix measured-temp over 25.5 (handling the tempadd messages better) + +- 2006-05-20 (2.2) + - FHZ1300 support verified (+ doc changes) + - KS300 support added (with Temperature, Humidity, Wind speed, Rain). + Not verified/undecoded: subzero temp, weak battery, Is-raining flag, + wind speed > 100km/h + - webpgm2 log fix for "offed" FHT devices (with no actuator data) + - webpgm3 upgrade (by Martin Haas, see webpgm/pgm3/docs/CHANGES for details) + - HMS logging/state format changed to make it similar to KS300 + - added HMS100WD (thanks to Sascha Pollok) + - ntfy/logging changed to be able to notify for multiple attributes + arriving in one message + - central FHTcode settable (see commandref.html) + - optionally listen for non-local requests (port global) + - unknown logging + - FAQ + +- 2006-04-15 (2.1) + - webfrontend/pgm2 changes: + - make it work on Asus dsl-routers (no "use warnings") + - css/readonly configurable + - Formatting for HMS data + - comments can be added to each device (setstate comment:xxx) + - testbed to dry-test functionality (test directory) + - added an empty hull for the KS300 weather module + - added undocumented "exec" function to call arbitrary program parts + for debugging. Example: exec FhzDecode("81xx04xx0101a0011234030011"); + - webfrontend/pgm3, contributed by Martin Haas + - fixed pgm1: changing values should work now + +- 2006-04-02 (2.0) + - XmlList and webfrontend/pgm1 programs from Raoul Matthiessen + - list tries to display the state and not the last command + - Both log facilities (FileLog and Log) take wildcards + (week, year, month, etc) to make logfile rotating easier + - webfrontend/pgm2 + +- 2006-02-12 (1.9b) + - Bugfix: Fixing the same bug again (thanks to Martin) + +- 2006-02-12 (1.9a) + - Bugfix: wrong rights for HMS and wrong place for readonly + (thanks to Juergen) + +- 2006-02-10 (1.9) + (aka as the Juergen release) + - The FHZ1300 is reported to work + - Bugfix: spaces before comment in the config file should be ignored + - added FS20STR codes to 10_FS20.pm + - names restricted to A-Za-z0-9.:- (especially _ is not allowed) + - delete calles now an UndefFn in the module + - implementation of FS20 function group/local master/global master + - the list command tells you the definition of the device too + +- 2006-01-05 (1.8) + - Bugfix: detailed FS20 status was not set from external event + - Bugfix: setstate for FS20 returned the last state set + - Bugfix: undefined FS20 devices (can) crash the server + - HMS module added by Martin Mueller + (currently supporting the HMS100T & HMS100TF) + - Log modules added, the first one being a simple FileLog + (inspired by Martin Mueller) + - A little gnuplot script to display temperature and actuator changes + +- 2006-01-04 (1.7) + - the at command can be used to execute something repeatedly with * + - ntfy can filter on device or on device+event with a regexp + - checking the delete and notify regexps if they make sense + - the FHT init string is now a set command (refreshvalues) + - shutdown saves the detailed device information too + +- 2006-01-03 (1.6) + - signal handling (to save the state on shutdown) + - module FHZ addded (for the FHZ1000PC device itself) + - added the get function (to make the initialization prettier) + - the module ST was renamed to FS20 + - FS20 timer commands added + - modules command removed (we are loading everything from the modpath + directory) + - FHT80b module added (yes, it is already useful, you can set + and view a lot of values) + - documentation adapted + - Added a TODO file + +- 2005-12-26 (1.5) + - "modularized" in preparation for the FHT80B -> each device has a type + - added relative "at" commands (with +HH:MM:SS) + - multiple commands on one line separated with ; + - sleeping 0.22 seconds after an ST command + - some commands/syntax changed: + - switch => set + - device => fhzdevice + - define ... => define ... + - the state of the devices and the at commands are saved + - at start always sending a "set 0001 00 01" to enable the FHZ receiever. + This is a workaround. + - doc rewrite, examples directory + +- 2005-11-10 (1.4) + - Reformatting the package and the documentation + - New links + +- 2005-10-27 (1.3) + - Bugfix: multiple at commands at the same time. diff --git a/fhem/FHEM/00_SONOS.pm b/fhem/FHEM/00_SONOS.pm new file mode 100755 index 000000000..43dfccb00 --- /dev/null +++ b/fhem/FHEM/00_SONOS.pm @@ -0,0 +1,6668 @@ +######################################################################################## +# +# SONOS.pm (c) by Reiner Leins, 2014 +# rleins at lmsoft dot de +# +# $Id$ +# +# FHEM module to commmunicate with a Sonos-System via UPnP +# +# !WARNING! +# This Module needs UPnP-Library +# Installation: +# * LWP::Simple +# * HTML::Entities +# * Net::Ping +# * File::Path +# * Time::HiRes +# * threads +# * Thread::Queue +# +# Internal Version 2.6 - December, 2014 +# +# define SONOS [[[interval] waittime] delaytime] +# +# where may be replaced by any name string +# is the connection identifier to the internal server. Normally "localhost" with a locally free port e.g. "localhost:4711". +# interval is the interval in s, for checking the existence of a ZonePlayer after definition +# waittime is the time to wait for the subprocess. defaults to 8. +# delaytime is the Time for delaying the network- and subprocess-part of this module. If the port is longer than neccessary blocked on the subprocess-side, it may be useful. +# +######################################################################################## +# Changelog +# +# 2.6: Die Zeichenkodierung bei Datenübernahme vom Zoneplayer kann nun über das Attribut characterDecoding eingestellt werden +# Bei Gruppen-/LineIn-/SPDIF-Wiedergabe wird wieder die liefernde Zone angezeigt (als Albumname) +# SetCurrentPlaylist hatte einen Tippfehler, und konnte dementsprechend nicht ausgeführt werden +# Unter Ubuntu gibt es die SHA1-Library nicht mehr, sodass man dort eine andere einbinden muss (SHA) +# Wenn bei den Methoden zum heraussuchen der FHEM-Devices etwas nicht gefunden wurde, dann wird jetzt eine Fehlermeldung mit dem gesuchten Merkmal ausgegeben +# Es können jetzt IP-Adressen von der UPnP-Verarbeitung ausgeschlossen werden +# Es wird nun ein fester Mimetype 'jpg' für Google Music und Simfy festgelegt +# Beim Alarm-Reading-Setzen wurde etwas doppelt gesetzt, was u.U. zu Fehlern führen konnte +# Die Read-Function wurde robuster gegen Übertragungsprobleme gemacht +# Das Wiederherstellen des Playerzustands nach einem PlayURITemp sieht nun auch den PlayBar-Eingang vor +# Es wird nun anstatt der WebCmd-Auflistung ein RemoteControl beim Erstellen der Komponenten erzeugt +# Wenn sich doch noch ein UPnP-Device als Player ausgibt, dann wird dies nun etwas sicherer erkannt +# Der Eingang einer Playbar kann nun auf anderen Playern wiedergegeben werden (mittels des Fhem-Namens) +# Lesen wurde auf DevIo_SimpleRead umgestellt (stand auf DevIo_DoSimpleRead). Dadurch wird das Fehlerhandling vereinfacht. +# Man kann die Zeit für das Warten auf den Subprozess nun beim Define mit angeben. Standardmäßig wird 8 verwendet. +# Es wird nun in regelmäßigen Abständen (Intervall wie bei der Prüfung der Sonosplayer) geprüft, ob die Verbindung zum Subprozess noch funktioniert +# Die Readings, die beim Start nicht geladen werden dürfen, werden beim Start von Fhem nun initialisiert. Damit wird die Fehlermeldung in MOTD verhindert +# Der Zeitstempel in der Konsolenausgabe berücksichtigt nun auch die Global-Angabe, ob Millisekunden mit ausgegeben werden sollen +# Der Start wurde komplett überarbeitet. Nun sind die einzelnen Wartebereiche in Timer ausgelagert, sodass Fhem nicht mit warten blockiert wird. +# Die Wiederherstellung des alten Playerzustands nach einem PlayURITemp (und damit auch bei Speak) wird nun auch bei Dateien gemacht, die mit 0s Dauer ermittelt werden (da sie sehr kurz sind). +# Der Aufruf der Google Text2Speech-Engine wird nun bei mehr als 95 Zeichen in mehrere Aufrufe aufgeteilt. Damit sind nun auch lange Texte über Google möglich, allerdings geht die Textmelodie u.U. verloren. +# Man kann jetzt für die Speak-Erzeugung ein JPG- oder PNG-Bild angeben. Dies kann für jedes Speak-Programm getrennt erfolgen. +# Beim Speak-Aufruf werden Umlaute nun auch korrekt an den Text2Speech-Generator (z.B. Google) übergeben, und korrekt in den MP3-Tag geschrieben +# Spotify-Cover werden nun in größerer Auflösung (meist 640x640 Pixel) direkt von Spotify heruntergeladen, und enthalten dann nicht mehr das Spotify-Logo +# Es gibt zwei neue Readings 'currentAlbumArtURL' und 'nextAlbumArtURL', die die Originalpfade zum eigenen Download darstellen +# Es gibt nun zwei Prozeduren, die als Grundlage oder Beispiel für die Verwendung von ReadingsGroups dienen können: 'SONOS_getTitleRG' und 'SONOS_getCoverRG' +# Beim automatischen Erzeugen der Sonos-Devices werden nun ReadingsGroups mit mehr Informationen erzeugt. Dies kann (und soll) auch als Vorlage für eigene Ideen Verwendet werden +# Es gibt eine weitere ReadingsGroup-Vorlage (steht auch im Wiki), mit der Listen (Playlisten, Favoriten und Radios) dargestellt werden können +# Es gibt zwei neue Attribute "proxyCacheTime" und "proxyCacheDir", die einen Cache im Proxy aktivieren +# Es gibt drei neue Getter am Sonosplayer-Device: "FavouritesWithCover", "PlaylistsWithCover" und "RadiosWithCovers". Diese geben eine Datenstruktur zurück, die den Titel und das Cover des Elements enthält. +# Die Prozeduren für die Anzeige des aktuellen und nächsten Titels verwenden nun ausschließlich DIV-Container (anstatt Tabellen). Dadurch klappt die Anzeige auch in einem Dashboard. +# Die Standard-ReadingsGroup-Anzeige durch die Prozeduren sind nun Parametrisiert. Man kann die minimale Breite der Anzeige sowie den Abstand zwischen aktuellem und nächstem Titel in Pixel festlegen +# Manche Sender (z.B. Capital Radio Türkiye) haben verbotene Newlines in den Titelinformationen mitgesendet. Diese werden nun entfernt. +# Man kann das Cover nun anklicken (oder antippen), und erhält dann die Coverdarstellung in einer Vollbilddarstellung mit Abspielstatus und Titelinformationen +# Es gibt zwei neue Befehle 'StartPlaylist' und 'StartRadio', die die gleichen Parameter wie ihre Pendants mit 'Load' am Anfang haben, nur dass hier das Abspielen gleich gestartet wird. +# Es gibt jetzt ein Reading 'currentTrackPosition', welches bei jedem Transportstate-Wechsel (neuer Titel, Play/Pause/Stop usw.) gesetzt wird. Damit kann man die verbleibende Restzeit eines laufenden Titels ermitteln, bzw. den Pausezeitpunkt anzeigen. +# Beim Wiedergeben von TV oder sonstigen externen Quellen, wird jetzt nicht mehr das 'leere' Cover angezeigt, sondern ein TV-Cover bzw. ein Default-Input-Cover +# Aufnahme in das offizielle Release von Fhem +# +# 2.5: Verwendung und Speicherung der Benutzer-IDs für Spotify und Napster wurden stabiler gegenüber Sonderzeichen gemacht +# Spotify-URLs werden im Reading 'currentTrackURI' und 'nextTrackURI' lesbarer abgelegt +# Ein Fehler beim Öffnen von M3U-Playlistdateien wurde behoben (dafür Danke an John) +# Überholt: Für die Informationsanfragen an Fhem durch den SubProzess wird nun standardmäßig der Telnet-Port von Fhem verwendet. Wenn das fehlschlägt, wird auf den alten Mechanismus zurückgeschaltet +# Neu: Es werden keine Informationsanfragen mehr zwischen Fhem und dem SubProzess ausgetauscht. Notwendige Informationen müssen vorher übertragen werden. Das bedeutet, dass bei einer Attributänderung ein Neustart von Fhem erfolgen muss. +# Es wurde ein Standard-Layout für das RemoteControl-Hilfsmodul angelegt +# Der Verbose-Level des Sonos-Devices wird nun auch an den SubProzess weitergereicht (auch zur Laufzeit), und beim initialen Start des SubProzess-Threads mitgegeben. +# AlbumArt von Napster erhält nun den festen Mimetype 'jpg', da dieser nicht übertragen wird +# Es werden nun die durch Fhem definierten Standard-Attribute mit angeboten +# Es gab ein Problem mit der Befehlsverarbeitung, wenn das Verbose-Attribut an einem Sonos-Device gesetzt war. +# Es wird nun auf Änderungsevents für den Zonennamen und das Zonenicon reagiert, und die entsprechenden Readings aktualisiert +# Es gibt jetzt zwei neue Setter: 'Name' und 'Icon', mit dem der Name und das Icon der Zone eingestellt werden kann +# Es gibt jetzt einen Getter 'PossibleRoomIcons', welcher die möglichen Angaben für den neuen Setter 'Icon' liefert +# Das Reading 'ZoneGroupID' wird nun auf eine andere Weise ermittelt und gesetzt +# Es gib jetzt ein neues Reading 'AlarmRunning', welches auf '1' steht, wenn gerade eine Alarmabspielung aktiv ist +# Die Namens- und Aufgabenerkennung beim Ermitteln der Player wurde angepasst +# Der Aufruf von AddMember und RemoveMember wurde bzgl. des SonosDevice-Namen abgesichert, sodass hier kein Absturz mehr bei einer falschen Deviceangabe erfolgt +# Es gibt jetzt ein neues Reading 'AlarmRunningID', welches bei einer Alarmausführung die ID des aktiven Alarms enthält +# Das Senden von Aktualisierungen an Fhem wurde etwas sicherer gemacht, wenn Fhem auf der anderen Seite gerade nicht zuhören kann +# Die Readings 'AlarmList', 'AlarmListIDs' und 'AlarmListVersion' werden nicht mehr aus dem Statefile geladen, da dort Sonderzeichen wie '#' zum Abschneiden der restlichen Zeile führen +# Anpassung der UPnP-Klasse, damit das Device-Beschreibungsdokument nur noch einmal geladen wird (anstatt wie bisher zweimal) +# Anpassung im Bereich der Cover Aktualisierung über FhemWeb. Das geht jetzt mit viel weniger Aufwand durch. +# Es gibt jetzt einen Setter 'SnapshotGroupVolume', der das aktuelle Lautstärkenverhältnis der einzelnen Player einer Gruppe für die folgenden Aufrufe des Setter 'GroupVolume' festhält. Die Anweisungen 'PlayURI' und 'PlayURITemp' (sowie darauf aufbauende Aufrufe wie 'Speak') führen diese Anweisung selbsttätig beim Starten durch. +# Wenn beim Auffrischen der Subscriptions ein Fehler auftritt, der darauf schließen läßt, dass der Player weg ist, dann wird die entsprechende Referenz aufgeräumt +# Man kann als relative Angabe bei setVolume nun einen Prozentwert angeben, z.B. '+20%'. Damit wird die Lautstärke um den jeweiligen prozentualen Anteil erhöht oder abgesenkt. +# Es gibt jetzt ein Reading 'LineInConnected', welches eine '1' enthält, wenn der Line-In-Eingang angeschlossen wurde, sonst '0'. +# +# 2.4: Initiale Lautstärkenermittlung wurde nun abgesichert, falls die Anfrage beim Player fehlschlägt +# Verbesserte Gruppenerkennung für die Anzeige der Informationen wie Titel usw. +# Fallback (Log) für den Aufruf von Log3 geschaffen, damit auch alte FHEM-Versionen funktionieren +# Es wurde eine Korrektur im verwendetetn UPnP-Modul gemacht, die eine bessere Verarbeitung der eingehenden Datagramme gewährleistet (dafür Danke an Sacha) +# Es werden nun zusätzliche Readings (beginnend mit 'next') mit den Informationen über den nächsten Titel befüllt. Diese können natürlich auch für InfoSummarize verwendet werden +# Es kann nun ein Eintrag aus der Sonos-Favoritenliste gestartet werden (Playlist oder Direkteintrag) +# Das Benennen der Sonos-Fhem-Devices wird nun auf Namensdoppelungen hin überprüft, und der Name eindeutig gemacht. Dabei wird im Normalfall das neue Reading 'fieldType' an den Namen angehangen. Nur der Master einer solchen Paarung bekommt dann den Original-Raumnamen als Fhem-Devicenamen +# Es gibt ein neues Reading 'fieldType', mit dem man erkennen kann, an welcher Position in einer Paarung dieser Zoneplayer steht +# Diverse Probleme mit Gruppen und Paarungen beim neu Erkennen der Sonos-Landschaft wurden beseitigt +# Es gibt jetzt einen Getter 'EthernetPortStatus', der den Status des gewünschten Ethernet-Ports liefert +# Es gibt jetzt einen Setter 'Reboot', der einen Neustart des Zoneplayers durchführt +# Es gibt jetzt einen Setter 'Wifi', mit dem der Zustand des Wifi-Ports eines Zoneplayers gesetzt werden kann +# Wenn ein Player als "Disappeared" erkannt wird, wird dem Sonos-System dies mitgeteilt, sodass er aus allen Listen und Controllern verschwindet +# Kleinere Korrektur, die eine bessere Verarbeitung der Kommunikation zwischen Fhem und dem Subprozess bewirkt +# +# 2.3: Die Antwort von 'SetCurrentPlaylist' wurde korrigiert. Dort kam vorher 'SetToCurrentPlaylist' zurück. +# VolumeStep kann nun auch als Attribut definiert werden. Das fehlte in der zulässigen Liste noch. +# Speak kann nun auch für lokale Binary-Aufrufe konfiguriert werden. +# Speak kann nun einen Hash-Wert auf Basis des gegebenen Textes in den Dateinamen einarbeiten, und diese dann bei Gleichheit wiederverwenden (Caching) +# Sonos kann nun ein "set StopAll" oder "set PauseAll" ausführen, um alle Player/Gruppen auf einen Schlag zu stoppen/pausieren +# Beim Discover-Event wird nun genauer geprüft, ob sich überhaupt ein ZonePlayer gemeldet hat +# Die UserIDs für Napster und Spotify werden wieder korrekt ermittelt. Damit kann auch wieder ein Playlistenimport erfolgen. +# Loudness Einstell- und Abfragbar +# Bass Einstell- und Abfragbar +# Treble Einstell- und Abfragbar +# Volume kann nun auch als RampToVolume ausgeführt werden +# +# 2.2: Befehlswarteschlange wieder ausgebaut. Dadurch gibt es nur noch das Reading LastActionResult, und alles wird viel zügiger ausgeführt, da Fhem nicht auf die Ausführung warten muss. +# TempPlaying berücksichtigt nun auch die Wiedergabe von Line-In-Eingängen (also auch Speak) +# Veraltete, mittlerweile unbenutzte, Readings werden nun gelöscht +# SetLEDState wurde hinzugefügt +# Die IsAlive-Überprüfung kann mit 'none' abgeschaltet werden +# CurrentTempPlaying wird nicht mehr benötigt +# +# 2.1: Neuen Befehl 'CurrentPlaylist' eingeführt +# +# 2.0: Neue Konzeptbasis eingebaut +# Man kann Gruppen auf- und wieder abbauen +# Es gibt neue Lautstärke- und Mute-Einstellungen für Gruppen ingesamt +# Man kann Button-Events definieren +# +# 1.13: Neuer Abspielzustand 'TRANSITIONING' wird berücksichtigt +# Der Aufruf von 'GetDeviceDefHash' wird nun mit dem Parameter 'undef' anstatt ohne einen Parameter durchgeführt +# +# 1.12: TrackURI hinzugefügt +# LoadPlayList und SavePlayList können nun auch Dateinamen annehmen, um eine M3U-Datei zu erzeugen/als Abspielliste zu laden +# Alarme können ausgelesen, gesetzt und gelöscht werden +# SleepTimer kann gesetzt und ausgelesen werden +# Reading DailyIndexRefreshTime hinzugefügt +# Bei AddURIToQueue und PlayURI können jetzt auch (wie bei LoadPlayList) Spotify und Napster-Ressourcen angegeben werden +# Beim Erzeugen des Cover-Weblinks wird nun nur noch die Breite festgelegt, damit Nicht-Quadratische Cover auch korrekt dargestellt werden +# SONOS_Stringify gibt Strings nun in einfachen Anführungszeichen aus (und maskiert etwaig enthaltene im String selbst) +# +# 1.11: Ein Transport-Event-Subscribing wird nur dann gemacht, wenn es auch einen Transport-Service gibt. Die Bridge z.B. hat sowas nicht. +# Bei PlayURITemp wird nun der Mute-Zustand auf UnMute gesetzt, und anschließend wiederhergestellt +# Shuffle, Repeat und CrossfadeMode können nun gesetzt und abgefragt werden. Desweiteren wird der Status beim Transport-Event aktualisiert. +# Umlaute bei "generateInfoSmmarize3" durch "sichere" Schreibweise ersetzt (Lautstärke -> Lautstaerke) +# +# 1.10: IsAlive beendet nicht mehr den Thread, wenn der Player nicht mehr erreichbar ist, sondern löscht nur noch die Proxy-Referenzen +# FHEMWEB-Icons werden nur noch im Hauptthread aktualisiert +# Getter 'getBalance' und Setter 'setBalance' eingeführt. +# HeadphoneConnected inkl. minVolumeHeadphone und maxVolumeHeadphone eingeführt +# InfoSummarize um die Möglichkeit der Volume/Balance/HeadphoneConnected-Felder erweitert. Außerdem werden diese Info-Felder nun auch bei einem Volume-Event neu berechnet (und triggern bei Bedarf auch!) +# InfoSummarize-Features erweitert: 'instead' und 'emptyval' hinzugefügt +# IsAlive prüft nicht mehr bei jedem Durchgang bis zum Thread runter, ob die Subscriptions erneuert werden müssen +# +# 1.9: RTL.it Informationen werden nun schöner dargestellt (Da steht eine XML-Struktur im Titel) +# Wenn kein Cover vom Sonos geliefert werden kann, wird das FHEM-Logo als Standard verwendet (da dieses sowieso auf dem Rechner vorliegt) +# UPnP-Fehlermeldungen eingebaut, um bei einer Nichtausführung nähere Informationen erhalten zu können +# +# 1.8: Device-Removed wird nun sicher ausgeführt. Manchmal bekommt man wohl deviceRemoved-Events ohne ein vorheriges deviceAdded-Event. Dann gibt es die gesuchte Referenz nicht. +# Renew-Subscriptions wurden zu spät ausgeführt. Da war alles schon abgelaufen, und konnte nicht mehr verlängert werden. +# ZonePlayer-Icon wird nun immer beim Discover-Event heruntergeladen. Damit wird es auch wieder aktualisiert, wenn FHEM das Icon beim Update verwirft. +# MinVolume und MaxVolume eingeführt. Damit kann nun der Lautstärkeregelbereich der ZonePlayer festgelegt werden +# Umlaute beim Übertragen in das Reading State werden wieder korrekt übertragen. Das Problem waren die etwaigen doppelten Anführungsstriche. Diese werden nun maskiert. +# Sonos Docks werden nun auch erkannt. Dieses hat eine andere Device-Struktur, weswegen der Erkennungsprozess angepasst werden musste. +# +# 1.7: Umlaute werden bei Playernamen beim Anlegen des Devices korrekt umgewandelt, und nicht in Unterstriche +# Renew-Subscription eingebaut, damit ein Player nicht die Verbindung zum Modul verliert +# CurrentTempPlaying wird nun auch sauber beim Abbrechen des Restore-Vorgangs zurückgesetzt +# Die Discovermechanik umgebaut, damit dieser Thread nach einem Discover nicht neu erzeugt werden muss. +# +# 1.6: Speak hinzugefügt (siehe Doku im Wiki) +# Korrektur von PlayURITemp für Dateien, für die Sonos keine Abspiellänge zur Verfügung stellt +# Korrektur des Thread-Problems welches unter *Nix-Varianten auftrat (Windows war nicht betroffen) +# +# 1.5: PlayURI, PlayURITemp und AddURIToQueue hinzugefügt (siehe Doku im Wiki) +# +# 1.4: Exception-Handling bei der Befehlsausführung soll FHEM besser vor verschwundenen Playern schützen +# Variable $SONOS_ThisThreadEnded sichert die korrekte Beendigung des vorhandenen Threads, trotz Discover-Events in der Pipeline +# Einrückungen im Code korrigiert +# +# 1.3: StopHandling prüft nun auch, ob die Referenz noch existiert +# +# 1.2: Proxy-Objekte werden beim Disappearen des Player entfernt, und sorgen bei einem nachfolgenden Aufruf für eine saubere Fehlermeldung +# Probleme mit Anführungszeichen " in Liedtiteln und Artist-Angaben. Diese Zeichen werden nun ersetzt +# Weblink wurde mit fehlendem "/" am Anfang angelegt. Dadurch hat dieser nicht im Floorplan funktionert +# pingType wird nun auf Korrektheit geprüft. +# Play:3 haben keinen Audio-Eingang, deshalb funktioniert das Holen eines Proxy dafür auch nicht. Jetzt ist das Holen abgesichert. +# +# 1.1: Ping-Methode einstellbar über Attribut 'pingType' +# +# 1.0: Initial Release +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +# Use-Declarations +######################################################################################## +package main; + +use strict; +use warnings; + +use Cwd qw(realpath); +use LWP::Simple; +use LWP::UserAgent; +use URI::Escape; +use HTML::Entities; +use Net::Ping; +use Socket; +use IO::Select; +use IO::Socket::INET; +use File::Path; +use File::stat; +use Time::HiRes qw(usleep gettimeofday); +use Scalar::Util qw(reftype looks_like_number); +use PerlIO::encoding; +use Encode; +use Digest::MD5 qw(md5_hex); +use File::Temp; + +use Data::Dumper; +$Data::Dumper::Terse = 1; + +use threads; +use Thread::Queue; +use threads::shared; + +use feature 'state'; + + +######################################################## +# IP-Adressen, die vom UPnP-Modul ignoriert werden sollen +######################################################## +my %IgnoreIPs = (); + + +######################################################## +# Standards aus FHEM einbinden +######################################################## +use vars qw{%attr %defs %intAt %data}; + + +######################################################## +# Prozeduren für den Betrieb des Standalone-Parts +######################################################## +sub Log($$); +sub Log3($$$); + +sub SONOS_Log($$$); +sub SONOS_StartClientProcessIfNeccessary($); +sub SONOS_Client_Notifier($); +sub SONOS_Client_ConsumeMessage($$); + +sub SONOS_RCLayout(); + + +######################################################## +# Verrenkungen um in allen Situationen das benötigte +# Modul sauber geladen zu bekommen.. +######################################################## +my $gPath = ''; +BEGIN { + $gPath = substr($0, 0, rindex($0, '/')); +} +if (lc(substr($0, -7)) eq 'fhem.pl') { + $gPath = $attr{global}{modpath}.'/FHEM'; +} +use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './'); +print 'Current: "'.$0.'", gPath: "'.$gPath."\"\n"; +use UPnP::ControlPoint; +require 'DevIo.pm' if (lc(substr($0, -7)) eq 'fhem.pl'); + + +######################################################################################## +# Variable Definitions +######################################################################################## +my %gets = ( + 'Groups' => '' +); + +my %sets = ( + 'Groups' => 'groupdefinitions', + 'StopAll' => '', + 'PauseAll' => '' +); + +my @SONOS_PossibleDefinitions = qw(NAME INTERVAL); +my @SONOS_PossibleAttributes = qw(targetSpeakFileHashCache targetSpeakFileTimestamp targetSpeakDir targetSpeakURL Speak0 Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone getAlarms disable generateVolumeEvent buttonEvents characterDecoding generateProxyAlbumArtURLs proxyCacheTime); +my @SONOS_PossibleReadings = qw(AlarmList AlarmListIDs UserID_Spotify UserID_Napster location SleepTimerVersion Mute HeadphoneConnected Balance Volume Loudness Bass Treble AlarmListVersion ZonePlayerUUIDsInGroup ZoneGroupID fieldType ZoneGroupName roomName roomIcon LineInConnected); + +# Obsolete Einstellungen... +my $SONOS_UseTelnetForQuestions = 1; +my $SONOS_UseTelnetForQuestions_Host = 'localhost'; # Wird automatisch durch den anfragenden Host ersetzt +my $SONOS_UseTelnetForQuestions_Port = 7072; + +# Communication between the two "levels" of threads +my $SONOS_ComObjectTransportQueue = Thread::Queue->new(); + +my %SONOS_PlayerRestoreRunningUDN :shared = (); +my $SONOS_PlayerRestoreQueue = Thread::Queue->new(); + +# For triggering the Main-Thread over Telnet-Session +my $SONOS_Thread :shared = -1; +my $SONOS_Thread_IsAlive :shared = -1; +my $SONOS_Thread_PlayerRestore :shared = -1; + +my %SONOS_Thread_IsAlive_Counter; +my $SONOS_Thread_IsAlive_Counter_MaxMerci = 2; + +# Some Constants +my @SONOS_PINGTYPELIST = qw(none tcp udp icmp syn); +my $SONOS_DEFAULTPINGTYPE = 'syn'; +my $SONOS_SUBSCRIPTIONSRENEWAL = 1800; +my $SONOS_DIDLHeader = ''; +my $SONOS_DIDLFooter = ''; +my $SONOS_GOOGLETRANSLATOR_CHUNKSIZE = 95; + +# Basis UPnP-Object und Search-Referenzen +my $SONOS_Controlpoint; +my $SONOS_Search; + +# ControlProxies für spätere Aufrufe für jeden ZonePlayer extra sichern +my %SONOS_AVTransportControlProxy; +my %SONOS_RenderingControlProxy; +my %SONOS_GroupRenderingControlProxy; +my %SONOS_ContentDirectoryControlProxy; +my %SONOS_AlarmClockControlProxy; +my %SONOS_AudioInProxy; +my %SONOS_DevicePropertiesProxy; +my %SONOS_GroupManagementProxy; +my %SONOS_MusicServicesProxy; +my %SONOS_ZoneGroupTopologyProxy; + +# Subscriptions müssen für die spätere Erneuerung aufbewahrt werden +my %SONOS_TransportSubscriptions; +my %SONOS_RenderingSubscriptions; +my %SONOS_AlarmSubscriptions; +my %SONOS_ZoneGroupTopologySubscriptions; +my %SONOS_DevicePropertiesSubscriptions; +my %SONOS_AudioInSubscriptions; + +# Locations -> UDN der einzelnen Player merken, damit die Event-Verarbeitung schneller geht +my %SONOS_Locations; + +# Wenn der Prozess/das Modul nicht von fhem aus gestartet wurde, dann versuchen, den ersten Parameter zu ermitteln +# Für diese Funktionalität werden einige Variablen benötigt +my $SONOS_ListenPort = $ARGV[0] if (lc(substr($0, -7)) ne 'fhem.pl'); +my $SONOS_Client_LogLevel = -1; +if ($ARGV[1]) { + $SONOS_Client_LogLevel = $ARGV[1]; +} +my $SONOS_mseclog = 0; +if ($ARGV[2]) { + $SONOS_mseclog = $ARGV[2]; +} +my $SONOS_StartedOwnUPnPServer = 0; +my $SONOS_Client_Selector; +my %SONOS_Client_Data :shared = (); +my $SONOS_Client_NormalQueueWorking :shared = 1; +my $SONOS_Client_SendQueue = Thread::Queue->new(); +my $SONOS_Client_SendQueue_Suspend :shared = 0; + +my %SONOS_ButtonPressQueue; + +######################################################################################## +# +# SONOS_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOS_Initialize ($) { + my ($hash) = @_; + # Provider + $hash->{Clients} = ':SONOSPLAYER:'; + + # Normal Defines + $hash->{DefFn} = 'SONOS_Define'; + $hash->{UndefFn} = 'SONOS_Undef'; + $hash->{ShutdownFn} = 'SONOS_Shutdown'; + $hash->{ReadFn} = "SONOS_Read"; + $hash->{ReadyFn} = "SONOS_Ready"; + $hash->{GetFn} = 'SONOS_Get'; + $hash->{SetFn} = 'SONOS_Set'; + $hash->{AttrFn} = 'SONOS_Attribute'; + $hash->{NotifyFn} = 'SONOS_Notify'; + + # CGI + my $name = "sonos"; + my $fhem_url = "/" . $name ; + $data{FWEXT}{$fhem_url}{FUNC} = "SONOS_FhemWebCallback"; + $data{FWEXT}{$fhem_url}{LINK} = $name; + $data{FWEXT}{$fhem_url}{NAME} = undef; + + eval { + no strict; + no warnings; + $hash->{AttrList}= 'pingType:'.join(',', @SONOS_PINGTYPELIST).' targetSpeakDir targetSpeakURL targetSpeakFileTimestamp:0,1 targetSpeakFileHashCache:0,1 Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover generateProxyAlbumArtURLs:0,1 proxyCacheTime proxyCacheDir characterDecoding '.$readingFnAttributes; + use strict; + use warnings; + }; + + $data{RC_layout}{Sonos} = "SONOS_RCLayout"; + + return undef; +} + +######################################################################################## +# +# SONOS_RCLayout - Returns the Standard-Layout-Definition for a RemoteControl-Device +# +######################################################################################## +sub SONOS_RCLayout() { + my @rows = (); + + push @rows, "Play:PLAY,Pause:PAUSE,Previous:REWIND,Next:FF,VolumeD:VOLDOWN,VolumeU:VOLUP,MuteT:MUTE"; + push @rows, "attr rc_iconpath icons/remotecontrol"; + push @rows, "attr rc_iconprefix black_btn_"; + + return @rows; +} + +######################################################################################## +# +# SONOS_getCoverTitleRG - Returns the Cover- and Title-Readings for use in a ReadingsGroup +# +######################################################################################## +sub SONOS_getCoverTitleRG($;$$) { + my ($device, $width, $space) = @_; + $width = 500 if (!defined($width)); + + my $transportState = ReadingsVal($device, 'transportState', ''); + + my $currentRuntime = 0; + my $currentStarttime = 0; + my $currentPosition = 0; + my $normalAudio = ReadingsVal($device, 'currentNormalAudio', 0); + if ($normalAudio) { + $currentRuntime = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackDuration', '0:00:01')); + $currentRuntime = 1 if (!$currentRuntime); + + $currentPosition = SONOS_GetTimeSeconds(ReadingsVal($device, 'currentTrackPosition', '0:00:00')); + + $currentStarttime = SONOS_GetTimeFromString(ReadingsTimestamp($device, 'currentTrackPosition', SONOS_TimeNow())) - $currentPosition; + } + + my $playing = 0; + if ($transportState eq 'PLAYING') { + $playing = 1; + $transportState = FW_makeImage('audio_play', 'Playing', 'SONOS_Transportstate'); + } + $transportState = FW_makeImage('audio_pause', 'Paused', 'SONOS_Transportstate') if ($transportState eq 'PAUSED_PLAYBACK'); + $transportState = FW_makeImage('audio_stop', 'Stopped', 'SONOS_Transportstate') if ($transportState eq 'STOPPED'); + + my $fullscreenDiv = '
'.ReadingsVal($device, 'roomName', $device).$transportState.'
'.ReadingsVal($device, 'infoSummarize1', '').'
'.(($normalAudio) ? '
' : '').'
'; + + my $javascriptTimer = 'function refreshTime'.$device.'() { + var playing = document.getElementById("prog_playing_'.$device.'"); + if (!playing || (playing && (playing.innerHTML == "0"))) { + return; + } + + var runtime = document.getElementById("prog_runtime_'.$device.'"); + var starttime = document.getElementById("prog_starttime_'.$device.'"); + if (runtime && starttime) { + var now = new Date().getTime(); + var percent = (Math.round(now / 10.0) - Math.round(starttime.innerHTML * 100.0)) / runtime.innerHTML; + document.getElementById("progressbar'.$device.'").style.width = percent + "%"; + + setTimeout(refreshTime'.$device.', 100); + } + }'; + + my $javascriptText = ''; + + $javascriptText =~ s/\n/ /g; + return $javascriptText.'
'.SONOS_getCoverRG($device).'
'.SONOS_getTitleRG($device, $space).'
'; +} + +######################################################################################## +# +# SONOS_getCoverRG - Returns the Cover-Readings for use in a ReadingsGroup +# +######################################################################################## +sub SONOS_getCoverRG($;$) { + my ($device) = @_; + + return ''; +} + +######################################################################################## +# +# SONOS_getTitleRG - Returns the Title-Readings for use in a ReadingsGroup +# +######################################################################################## +sub SONOS_getTitleRG($;$) { + my ($device, $space) = @_; + $space = 20 if (!defined($space)); + + my $infoString = ''; + + my $transportState = ReadingsVal($device, 'transportState', ''); + $transportState = 'Spiele' if ($transportState eq 'PLAYING'); + $transportState = 'Pausiere' if ($transportState eq 'PAUSED_PLAYBACK'); + $transportState = 'Stop bei' if ($transportState eq 'STOPPED'); + + # Läuft Radio oder ein "normaler" Titel + if (ReadingsVal($device, 'currentNormalAudio', 1) == 1) { + my $showNext = ReadingsVal($device, 'nextTitle', '') || ReadingsVal($device, 'nextArtist', '') || ReadingsVal($device, 'nextAlbum', ''); + $infoString = sprintf('
%s Titel %s von %s
Titel: %s
Interpret: %s
Album: %s'.($showNext ? '
Nächste Wiedergabe:
Titel: %s
Artist: %s
Album: %s
' : ''), + $transportState, + ReadingsVal($device, 'currentTrack', ''), + ReadingsVal($device, 'numberOfTracks', ''), + ReadingsVal($device, 'currentTitle', ''), + ReadingsVal($device, 'currentArtist', ''), + ReadingsVal($device, 'currentAlbum', ''), + $space, + ReadingsVal($device, 'nextAlbumArtURL', ''), + ReadingsVal($device, 'nextTitle', ''), + ReadingsVal($device, 'nextArtist', ''), + ReadingsVal($device, 'nextAlbum', '')); + } else { + $infoString = sprintf('
%s Radiostream
Sender: %s
Info: %s
Läuft: %s
', + $transportState, + ReadingsVal($device, 'currentSender', ''), + ReadingsVal($device, 'currentSenderInfo', ''), + ReadingsVal($device, 'currentSenderCurrent', '')); + } + + return $infoString; +} + +######################################################################################## +# +# SONOS_getListRG - Returns the approbriate list-Reading for use in a ReadingsGroup +# +######################################################################################## +sub SONOS_getListRG($$;$) { + my ($device, $reading, $ul) = @_; + $ul = 0 if (!defined($ul)); + + my $resultString = ''; + + # Manchmal ist es etwas komplizierter mit den Zeichensätzen... + my %elems = %{eval(decode('CP1252', ReadingsVal($device, $reading, '{}')))}; + + for my $key (keys %elems) { + my $command; + if ($reading eq 'Favourites') { + $command = 'cmd.'.$device.uri_escape('=set '.$device.' StartFavourite '.uri_escape($elems{$key}->{Title})); + } elsif ($reading eq 'Playlists') { + $command = 'cmd.'.$device.uri_escape('=set '.$device.' StartPlaylist '.uri_escape($elems{$key}->{Title})); + } elsif ($reading eq 'Radios') { + $command = 'cmd.'.$device.uri_escape('=set '.$device.' StartRadio '.uri_escape($elems{$key}->{Title})); + } + $command = "FW_cmd('/fhem?XHR=1&$command')"; + + if ($ul) { + $resultString .= '
  • '; + } else { + $resultString .= ''.$elems{$key}->{Title}."\n"; + } + } + + if ($ul) { + return '
      '.$resultString.'
    '; + } else { + return ''.$resultString.'
    '; + } +} + +######################################################################################## +# +# SONOS_getGroupsRG - Returns a simple group-constellation-list for use in a ReadingsGroup +# +######################################################################################## +sub SONOS_getGroupsRG() { + my $groups = CommandGet(undef, SONOS_getDeviceDefHash(undef)->{NAME}.' Groups'); + + my $result = '
      '; + my $i = 0; + while ($groups =~ m/\[(.*?)\]/ig) { + my @member = split(/, /, $1); + @member = map FW_makeImage('icoSONOSPLAYER_icon-'.ReadingsVal($_, 'playerType', '').'.png', '', '').ReadingsVal($_, 'roomName', $_), @member; + + $result .= '
    • '.++$i.'. Gruppe:
      • '.join('
      • ', @member).'
    • '; + } + return $result.'
    '; +} + +######################################################################################## +# +# SONOS_FhemWebCallback - Implements a Webcallback e.g. a small proxy for Cover-images. +# +######################################################################################## +sub SONOS_FhemWebCallback($) { + my ($URL) = @_; + + # Einfache Grundprüfungen + return ("text/html; charset=UTF8", 'Fehlerhafte Anfrage: '.$URL) if ($URL !~ m/^\/sonos\//i); + $URL =~ s/^\/sonos//i; + + # Proxy-Features... + if ($URL =~ m/^\/proxy\//i) { + return ("text/html; charset=UTF8", 'Proxybetrieb nicht aktiviert: '.$URL) if (!AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'generateProxyAlbumArtURLs', 0)); + + my $proxyCacheTime = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'proxyCacheTime', 0); + my $proxyCacheDir = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'proxyCacheDir', '/tmp'); + $proxyCacheDir =~ s/\\/\//g; + + # Zurückzugebende Adresse ermitteln... + my $albumurl = uri_unescape($1) if ($URL =~ m/^\/proxy\/aa\?url=(.*)/i); + + # Nur für Sonos-Player den Proxy spielen (und für Spotify-Links) + my $ip = ''; + $ip = $1 if ($albumurl =~ m/^http:\/\/(.*?)[:\/]/i); + for my $player (SONOS_getAllSonosplayerDevices()) { + if (ReadingsVal($player->{NAME}, 'location', '') =~ m/^http:\/\/$ip:/i) { + undef($ip); + last; + } + } + return ("text/html; charset=UTF8", 'Anfrage für Nicht-Sonos-Player: '.$URL) if ($ip && $URL !~ /\/original\//i); + + # Generierter Dateiname für die Cache-Funktionalitaet + my $albumHash; + + # Schauen, ob die Datei aus dem Cache bedient werden kann... + if ($proxyCacheTime) { + eval { + require Digest::SHA1; + import Digest::SHA1 qw(sha1_hex); + $albumHash = $proxyCacheDir.'/SonosProxyCache_'.sha1_hex(lc($albumurl)).'.image'; + }; + + if ((-e $albumHash) && ((stat($albumHash)->mtime) + $proxyCacheTime > gettimeofday())) { + SONOS_Log undef, 5, 'Cover wird aus Cache bedient: '.$albumHash.' ('.$albumurl.')'; + + $albumHash =~ m/(.*)\/(.*)\.(.*)/; + FW_serveSpecial($2, $3, $1, 1); + + return(undef, undef); + } + } + + # Bild vom Player holen... + my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11'); + my $response = $ua->get($albumurl); + if ($response->is_success) { + SONOS_Log undef, 5, 'Cover wurde neu geladen: '.$albumurl; + + my $tempFile; + if ($proxyCacheTime) { + unlink $albumHash if (-e $albumHash); + SONOS_Log undef, 5, 'Cover wird im Cache abgelegt: '.$albumHash.' ('.$albumurl.')'; + } else { + # Da wir die Standard-Prozedur 'FW_serveSpecial' aus 'FHEMWEB' verwenden moechten, brauchen wir eine lokale Datei + $tempFile = File::Temp->new(SUFFIX => '.image'); + $albumHash = $tempFile->filename; + $albumHash =~ s/\\/\//g; + SONOS_Log undef, 5, 'TempFilename: '.$albumHash; + } + + # Either Tempfile or Cachefile... + SONOS_WriteFile($albumHash, $response->content); + + $albumHash =~ m/(.*)\/(.*)\.(.*)/; + FW_serveSpecial($2, $3, $1, 1); + + return (undef, undef); + } + } + + # Cover-Features... + if ($URL =~ m/^\/cover\//i) { + $URL =~ s/^\/cover//i; + + if ($URL =~ m/^\/empty.jpg/i) { + FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1); + return (undef, undef); + } + + if ($URL =~ m/^\/playlist.jpg/i) { + return FW_serveSpecial('sonos_playlist', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1); + return (undef, undef); + } + + if ($URL =~ m/^\/input_default.jpg/i) { + return FW_serveSpecial('sonos_input_default', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1); + return (undef, undef); + } + + if ($URL =~ m/^\/input_tv.jpg/i) { + return FW_serveSpecial('sonos_input_tv', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1); + return (undef, undef); + } + + # Wird im Prinzip einfach nicht mehr aufgerufen... + #if ($URL =~ m/^\/favorit\?res=(.*)/i) { + # my $resURL = uri_unescape($1); + # + # if ($resURL =~ m/^(x-rincon-cpcontainer|x-sonos-spotify).*?(spotify.*?)(\?|$)/i) { + # my $infos = get('https://embed.spotify.com/oembed/?url='.$2); + # + # if ($infos =~ m/"thumbnail_url":"(.*?)cover(.*?)"/i) { + # $resURL = $1.'original'.$2; + # $resURL =~ s/\\//g; + # } + # } elsif($URL =~ m/savedqueues.rsq/i) { + # $resURL = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_playlist.jpg'; + # } else { + # my @player = SONOS_getAllSonosplayerDevices(); + # my $stream = 0; + # $stream = 1 if ($resURL =~ /x-sonosapi-stream/); + # $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.uri_escape($resURL) if (ReadingsVal($player[0]->{NAME}, 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i); + # } + # + # if ($resURL) { + # SONOS_Log undef, 5, 'Hole Cover: '.$resURL; + # + # if ($resURL =~ /^http/) { + # # Bild holen... + # my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11'); + # my $response = $ua->get($resURL); + # if ($response->is_success) { + # return ($response->header('Content-Type').'; charset=UTF8', $response->content); + # } + # } else { + # return ('charset=UTF8', SONOS_ReadFile($resURL)); + # } + # } + #} + } + + # Wenn wir hier ankommen, dann konnte nichts verarbeitet werden... + return ("text/html; charset=UTF8", 'Fehlerhafte Anfrage: '.$URL); +} + +######################################################################################## +# +# SONOS_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed +# def = definition string +# +######################################################################################## +sub SONOS_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + + # check syntax + return 'Usage: define SONOS [[[[upnplistener] interval] waittime] delaytime]' if($#a < 2 || $#a > 5); + my $name = $a[0]; + + my $upnplistener; + if ($a[2] && !looks_like_number($a[2])) { + $upnplistener = $a[2]; + } else { + $upnplistener = 'localhost:4711'; + } + + my $interval; + if (looks_like_number($a[3])) { + $interval = $a[3]; + if ($interval < 10) { + SONOS_Log undef, 0, 'Interval has to be a minimum of 10 sec. and not: '.$interval; + $interval = 10; + } + } else { + $interval = 10; + } + + my $waittime; + if (looks_like_number($a[4])) { + $waittime = $a[4]; + } else { + $waittime = 8; + } + + my $delaytime; + if (looks_like_number($a[5])) { + $delaytime = $a[5]; + } else { + $delaytime = 0; + } + + $hash->{NAME} = $name; + $hash->{DeviceName} = $upnplistener; + $hash->{INTERVAL} = $interval; + $hash->{WAITTIME} = $waittime; + $hash->{DELAYTIME} = $delaytime; + $hash->{STATE} = 'waiting for subprocess...'; + + if ($hash->{DELAYTIME}) { + InternalTimer(gettimeofday() + $hash->{DELAYTIME}, 'SONOS_DelayStart', $hash, 0); + } else { + SONOS_DelayStart($hash); + } + + return undef; +} + +######################################################################################## +# +# SONOS_DelayStart - Starts the SubProcess with a Delay. Can solute problems with blocked Ports +# +######################################################################################## +sub SONOS_DelayStart($) { + my ($hash) = @_; + + # Prüfen, ob ein Server erreichbar wäre, und wenn nicht, einen Server starten + SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}); + + InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0); +} + +######################################################################################## +# +# SONOS_DelayOpenDev - Starts the IO-Connection with a Delay. +# +######################################################################################## +sub SONOS_DelayOpenDev($) { + my ($hash) = @_; + + # Die Datenverbindung zu dem gemachten Server hier starten und initialisieren + DevIo_OpenDev($hash, 0, "SONOS_InitClientProcessLater"); +} + +######################################################################################## +# +# SONOS_Attribute - Implements AttrFn function +# +######################################################################################## +sub SONOS_Attribute($$$@) { + my ($mode, $devName, $attrName, $attrValue) = @_; + + if ($mode eq 'set') { + if ($attrName eq 'verbose') { + SONOS_DoWork('undef', 'setVerbose', $attrValue); + } + } + + return undef; +} + +######################################################################################## +# +# SONOS_Notify - Implements NotifyFn function +# +######################################################################################## +sub SONOS_Notify() { + my ($hash, $notifyhash) = @_; + + return undef; + + #if (($notifyhash->{NAME} eq 'global') && ($notifyhash->{CHANGED}[0] eq 'REREADCFG')) { + # SONOS_Log undef, 0, 'Detecting rereadcfg. Restart Sonos-Subprocess...'; + # + # # Vorab warten, da im Hintergrund der Thread noch läuft und auch erstmal fertig werden muss... + # select(undef, undef, undef, $hash->{WAITTIME}); + # + # # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden... + # RemoveInternalTimer($hash); + # DevIo_SimpleWrite($hash, "disconnect\n", 0); + # DevIo_CloseDev($hash); + # + # # Neu anstarten... + # SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer); + # InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0); + #} + # + #return undef; +} + +######################################################################################## +# +# SONOS_Ready - Implements ReadyFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOS_Ready($) { + my ($hash) = @_; + + return DevIo_OpenDev($hash, 1, "SONOS_InitClientProcessLater"); +} + +######################################################################################## +# +# SONOS_Read - Implements ReadFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOS_Read($) { + my ($hash) = @_; + + # Bis zum letzten (damit der Puffer leer ist) Zeilenumbruch einlesen, da SimpleRead immer nur 256-Zeichen-Päckchen einliest. + my $buf = DevIo_DoSimpleRead($hash); + + # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden... + if (!defined($buf) || ($buf eq '')) { + SONOS_Log undef, 1, 'Nothing could be read from TCP-Channel (the first level) even though the Read-Function was called.'; + + # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden... + RemoveInternalTimer($hash); + DevIo_SimpleWrite($hash, "disconnect\n", 0); + DevIo_CloseDev($hash); + + # Neu anstarten... + SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer); + InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0); + + return; + } + + # Wenn noch nicht alles gekommen ist, dann hier auf den Rest warten... + while (substr($buf, -1, 1) ne "\n") { + my $newRead = DevIo_SimpleRead($hash); + + # Wenn hier gar nichts gekommen ist, dann diesen Aufruf beenden... + if (!defined($newRead) || ($newRead eq '')) { + SONOS_Log undef, 1, 'Nothing could be read from TCP-Channel (the second level) even though the Read-Function was called. The client is now directed to shutdown and the connection should be re-initialized...'; + + # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden... + RemoveInternalTimer($hash); + DevIo_SimpleWrite($hash, "disconnect\n", 0); + DevIo_CloseDev($hash); + + # Neu anstarten... + SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer); + InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0); + + return; + } + + # Wenn es neue Daten gibt, dann anhängen... + $buf .= $newRead; + } + + # Die aktuellen Abspielinformationen werden Schritt für Schritt übertragen, gesammelt und dann in einem Rutsch ausgewertet. + # Dafür eignet sich eine Sub-Statische Variable am Besten + state %current; + + # Hier könnte jetzt eine ganze Liste von Anweisungen enthalten sein, die jedoch einzeln verarbeitet werden müssen + # Dabei kann der Trenner ein Zeilenumbruch sein, oder ein Tab-Zeichen. + foreach my $line (split(/[\n\a]/, $buf)) { + # Abschließende Zeilenumbrüche abschnippeln + $line =~ s/[\r\n]*$//; + + SONOS_Log undef, 5, "Received from UPnP-Server: '$line'"; + + # Hier empfangene Werte verarbeiten + if ($line =~ m/^ReadingsSingleUpdateIfChanged:(.*?):(.*?):(.*)/) { + if (lc($1) eq 'undef') { + SONOS_readingsSingleUpdateIfChanged(SONOS_getDeviceDefHash(undef), $2, $3, 1); + } else { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 1); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChanged: $1:$2:$3"; + } + } + } elsif ($line =~ m/^ReadingsSingleUpdateIfChangedNoTrigger:(.*?):(.*?):(.*)/) { + if (lc($1) eq 'undef') { + SONOS_readingsSingleUpdateIfChanged(SONOS_getDeviceDefHash(undef), $2, $3, 0); + } else { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + SONOS_readingsSingleUpdateIfChanged($hash, $2, $3, 0); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdateIfChangedNoTrigger: $1:$2:$3"; + } + } + } elsif ($line =~ m/^ReadingsSingleUpdate:(.*?):(.*?):(.*)/) { + if (lc($1) eq 'undef') { + readingsSingleUpdate(SONOS_getDeviceDefHash(undef), $2, $3, 1); + } else { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + readingsSingleUpdate($hash, $2, $3, 1); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsSingleUpdate: $1:$2:$3"; + } + } + } elsif ($line =~ m/^ReadingsBulkUpdate:(.*?):(.*?):(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + readingsBulkUpdate($hash, $2, $3); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdate: $1:$2:$3"; + } + } elsif ($line =~ m/^ReadingsBulkUpdateIfChanged:(.*?):(.*?):(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + SONOS_readingsBulkUpdateIfChanged($hash, $2, $3); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBulkUpdateIfChanged: $1:$2:$3"; + } + } elsif ($line =~ m/ReadingsBeginUpdate:(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + readingsBeginUpdate($hash); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsBeginUpdate: $1"; + } + } elsif ($line =~ m/ReadingsEndUpdate:(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + readingsEndUpdate($hash, 1); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ReadingsEndUpdate: $1"; + } + } elsif ($line =~ m/CommandDefine:(.*)/) { + CommandDefine(undef, $1); + } elsif ($line =~ m/CommandAttr:(.*)/) { + CommandAttr(undef, $1); + } elsif ($line =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + %current = SONOS_GetReadingsToCurrentHash($hash->{NAME}, $2); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von GetReadingsToCurrentHash: $1:$2"; + } + } elsif ($line =~ m/SetCurrent:(.*?):(.*)/) { + $current{$1} = $2; + } elsif ($line =~ m/CurrentBulkUpdate:(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + readingsBeginUpdate($hash); + + # Dekodierung durchführen + $current{Title} = decode_entities($current{Title}); + $current{Artist} = decode_entities($current{Artist}); + $current{Album} = decode_entities($current{Album}); + $current{AlbumArtist} = decode_entities($current{AlbumArtist}); + + $current{Sender} = decode_entities($current{Sender}); + $current{SenderCurrent} = decode_entities($current{SenderCurrent}); + $current{SenderInfo} = decode_entities($current{SenderInfo}); + + $current{nextTitle} = decode_entities($current{nextTitle}); + $current{nextArtist} = decode_entities($current{nextArtist}); + $current{nextAlbum} = decode_entities($current{nextAlbum}); + $current{nextAlbumArtist} = decode_entities($current{nextAlbumArtist}); + + SONOS_readingsBulkUpdateIfChanged($hash, "transportState", $current{TransportState}); + SONOS_readingsBulkUpdateIfChanged($hash, "Shuffle", $current{Shuffle}); + SONOS_readingsBulkUpdateIfChanged($hash, "Repeat", $current{Repeat}); + SONOS_readingsBulkUpdateIfChanged($hash, "CrossfadeMode", $current{CrossfadeMode}); + SONOS_readingsBulkUpdateIfChanged($hash, "SleepTimer", $current{SleepTimer}); + SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunning", $current{AlarmRunning}); + SONOS_readingsBulkUpdateIfChanged($hash, "AlarmRunningID", $current{AlarmRunningID}); + SONOS_readingsBulkUpdateIfChanged($hash, "numberOfTracks", $current{NumberOfTracks}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", $current{Track}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", $current{TrackURI}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", $current{TrackDuration}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", $current{TrackPosition}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", $current{Title}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", $current{Artist}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", $current{Album}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", $current{OriginalTrackNumber}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", $current{AlbumArtist}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtURL", $current{AlbumArtURL}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentSender", $current{Sender}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderCurrent", $current{SenderCurrent}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentSenderInfo", $current{SenderInfo}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", $current{StreamAudio}); + SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", $current{NormalAudio}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", $current{nextTrackDuration}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", $current{nextTrackURI}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", $current{nextTitle}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", $current{nextArtist}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", $current{nextAlbum}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtist", $current{nextAlbumArtist}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbumArtURL", $current{nextAlbumArtURL}); + SONOS_readingsBulkUpdateIfChanged($hash, "nextOriginalTrackNumber", $current{nextOriginalTrackNumber}); + SONOS_readingsBulkUpdateIfChanged($hash, "Volume", $current{Volume}); + SONOS_readingsBulkUpdateIfChanged($hash, "Mute", $current{Mute}); + SONOS_readingsBulkUpdateIfChanged($hash, "Balance", $current{Balance}); + SONOS_readingsBulkUpdateIfChanged($hash, "HeadphoneConnected", $current{HeadphoneConnected}); + + my $name = $hash->{NAME}; + + # If the SomethingChanged-Event should be triggered, do so. It's useful if one would be triggered if even some changes are made, and it's unimportant to exactly know what + if (AttrVal($name, 'generateSomethingChangedEvent', 0) == 1) { + readingsBulkUpdate($hash, "somethingChanged", 1); + } + + # If the Info-Summarize is configured to be triggered. Here one can define a single information-line with all the neccessary informations according to the type of Audio + SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize1', 1); + SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize2', 1); + SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize3', 1); + SONOS_ProcessInfoSummarize($hash, \%current, 'InfoSummarize4', 1); + + # Zusätzlich noch den STATE und das Reading State mit dem vom Anwender gewünschten Wert aktualisieren, Dabei müssen aber doppelte Anführungszeichen vorher maskiert werden... + SONOS_readingsBulkUpdateIfChanged($hash, 'state', $current{AttrVal($name, 'stateVariable', 'TransportState')}); + + # End the Bulk-Update, and trigger events + SONOS_readingsEndUpdate($hash, 1); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von CurrentBulkUpdate: $1"; + } + } elsif ($line =~ m/ProcessCover:(.*?):(.*?):(.*?):(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + if ($hash) { + my $name = $hash->{NAME}; + + my $nextReading = 'current'; + my $nextName = ''; + if ($2) { + $nextReading = 'next'; + $nextName = 'Next'; + } + + my $tempURI = $3; + my $groundURL = $4; + my $currentValue; + + my $srcURI = ''; + if (defined($tempURI) && $tempURI ne '') { + if ($tempURI =~ m/getaa.*?x-sonos-spotify.*?(spotify.*)%3f/i) { + my $infos = get('https://embed.spotify.com/oembed/?url='.$1); + + if ($infos =~ m/"thumbnail_url":"(.*?)cover(.*?)"/i) { + $srcURI = $1.'original'.$2; + $srcURI =~ s/\\//g; + $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg'; + SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');"; + } else { + $srcURI = $groundURL.$tempURI; + $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI); + SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download failed. Use normal thumbnail: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');"; + } + } elsif ($tempURI =~ m/^\/fhem\/sonos\/cover\/(.*)/i) { + $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_'.$1; + $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.jpg'; + SONOS_Log undef, 4, "Transport-Event: Cover-Copy: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');"; + } else { + $srcURI = $groundURL.$tempURI; + $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI); + SONOS_Log undef, 4, "Transport-Event: Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');"; + } + } else { + $srcURI = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_empty.jpg'; + $currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.png'; + SONOS_Log undef, 4, "Transport-Event: CoverArt konnte nicht gefunden werden. Verwende FHEM-Logo. Bilder-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');"; + } + mkpath($attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'); + my $filechanged = SONOS_DownloadReplaceIfChanged($srcURI, $currentValue); + # Icons neu einlesen lassen, falls die Datei neu ist + SONOS_RefreshIconsInFHEMWEB('/www/images/default/SONOSPLAYER/') if ($filechanged); + + # Die URL noch beim aktuellen Titel mitspeichern + my $URL = $srcURI; + if ($URL =~ m/\/lib\/UPnP\/sonos_(.*)/i) { + $URL = '/fhem/sonos/cover/'.$1; + } else { + my $sonosName = SONOS_getDeviceDefHash(undef)->{NAME}; + $URL = '/fhem/sonos/proxy/aa?url='.uri_escape($URL) if (AttrVal($sonosName, 'generateProxyAlbumArtURLs', 0)); + } + + if ($nextReading eq 'next') { + $current{nextAlbumArtURL} = $URL; + } else { + $current{AlbumArtURL} = $URL; + } + + # This URI change rarely, but the File itself change nearly with every song, so trigger it everytime the content was different to the old one + if ($filechanged) { + readingsSingleUpdate($hash, $nextReading.'AlbumArtURI', $currentValue, 1); + } else { + SONOS_readingsSingleUpdateIfChanged($hash, $nextReading.'AlbumArtURI', $currentValue, 1); + } + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von ProcessCover: $1:$2:$3:$4"; + } + } elsif ($line =~ m/^SetAlarm:(.*?):(.*?);(.*?):(.*)/) { + my $hash = SONOS_getSonosPlayerByUDN($1); + + my @alarmIDs = split(/,/, $3); + + if ($4) { + readingsSingleUpdate($hash, 'AlarmList', $4, 0); + } else { + readingsSingleUpdate($hash, 'AlarmList', '{}', 0); + } + SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListIDs', join(',', sort {$a <=> $b} @alarmIDs), 0); + SONOS_readingsSingleUpdateIfChanged($hash, 'AlarmListVersion', $2, 1); + } elsif ($line =~ m/QA:(.*?):(.*?):(.*)/) { # Wenn ein QA (Question-Attribut) gefordert wurde, dann auch zurückliefern + DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0); + } elsif ($line =~ m/QR:(.*?):(.*?):(.*)/) { # Wenn ein QR (Question-Reading) gefordert wurde, dann auch zurückliefern + DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0); + } elsif ($line =~ m/QD:(.*?):(.*?):(.*)/) { # Wenn ein QD (Question-Definition) gefordert wurde, dann auch zurückliefern + DevIo_SimpleWrite($hash, SONOS_AnswerQuery($line), 0); + } elsif ($line =~ m/DoWorkAnswer:(.*?):(.*?):(.*)/) { + my $chash; + if (lc($1) eq 'undef') { + $chash = SONOS_getDeviceDefHash(undef); + } else { + $chash = SONOS_getSonosPlayerByUDN($1); + } + + if ($chash) { + SONOS_Log undef, 4, "DoWorkAnswer arrived for ".$chash->{NAME}."->$2: '$3'"; + readingsSingleUpdate($chash, $2, $3, 1); + } else { + SONOS_Log undef, 0, "Fehlerhafter Aufruf von DoWorkAnswer: $1:$2:$3"; + } + } else { + SONOS_DoTriggerInternal('Main', $line); + } + } +} + +######################################################################################## +# +# SONOS_AnswerQuery - Create the approbriate answer for the given Question +# +# Parameter line = The line of Question +# +######################################################################################## +sub SONOS_AnswerQuery($) { + my ($line) = @_; + + if ($line =~ m/QA:(.*?):(.*?):(.*)/) { # Wenn ein QA (Question-Attribut) gefordert wurde, dann auch zurückliefern + my $chash; + if (lc($1) eq 'undef') { + $chash = SONOS_getDeviceDefHash(undef); + } else { + $chash = SONOS_getSonosPlayerByUDN($1); + } + + if ($chash) { + SONOS_Log undef, 4, "QA-Anfrage(".$chash->{NAME}."): $1:$2:$3"; + return "A:$1:$2:".AttrVal($chash->{NAME}, $2, $3)."\r\n"; + } else { + SONOS_Log undef, 1, "Fehlerhafte QA-Anfrage: $1:$2:$3"; + return "A:$1:$2:$3\r\n"; + } + } elsif ($line =~ m/QR:(.*?):(.*?):(.*)/) { # Wenn ein QR (Question-Reading) gefordert wurde, dann auch zurückliefern + my $chash; + if (lc($1) eq 'undef') { + $chash = SONOS_getDeviceDefHash(undef); + } else { + $chash = SONOS_getSonosPlayerByUDN($1); + } + + if ($chash) { + SONOS_Log undef, 4, "QR-Anfrage(".$chash->{NAME}."): $1:$2:$3"; + return "R:$1:$2:".ReadingsVal($chash->{NAME}, $2, $3)."\r\n"; + } else { + SONOS_Log undef, 1, "Fehlerhafte QR-Anfrage: $1:$2:$3"; + return "R:$1:$2:$3\r\n"; + } + } elsif ($line =~ m/QD:(.*?):(.*?):(.*)/) { # Wenn ein QD (Question-Definition) gefordert wurde, dann auch zurückliefern + my $chash; + if (lc($1) eq 'undef') { + $chash = SONOS_getDeviceDefHash(undef); + } else { + $chash = SONOS_getSonosPlayerByUDN($1); + } + + if ($chash) { + SONOS_Log undef, 4, "QD-Anfrage(".$chash->{NAME}."): $1:$2:$3"; + if ($chash->{$2}) { + return "D:$1:$2:".$chash->{$2}."\r\n"; + } else { + return "D:$1:$2:$3\r\n"; + } + } else { + SONOS_Log undef, 1, "Fehlerhafte QD-Anfrage: $1:$2:$3"; + return "D:$1:$2:$3\r\n"; + } + } +} + +######################################################################################## +# +# SONOS_StartClientProcess - Starts the client-process (in a forked-subprocess), which handles all UPnP-Messages +# +# Parameter port = Portnumber to what the client have to listen for +# +######################################################################################## +sub SONOS_StartClientProcessIfNeccessary($) { + my ($upnplistener) = @_; + my ($host, $port) = split(/:/, $upnplistener); + + my $socket = new IO::Socket::INET(PeerAddr => $upnplistener, Proto => 'tcp'); + if (!$socket) { + # Sonos-Device ermitteln... + my $hash = SONOS_getDeviceDefHash(undef); + + SONOS_Log undef, 1, 'Kein UPnP-Server gefunden... Starte selber einen und warte '.$hash->{WAITTIME}.' Sekunde(n) darauf...'; + $SONOS_StartedOwnUPnPServer = 1; + + if (fork() == 0) { + # Zuständigen Verbose-Level ermitteln... + # Allerdings sind die Attribute (momentan) zu diesem Zeitpunkt noch nicht gesetzt, sodass nur das globale Attribut verwendet werden kann... + my $verboselevel = AttrVal(SONOS_getDeviceDefHash(undef)->{NAME}, 'verbose', $attr{global}{verbose}); + + # Prozess anstarten... + exec('perl '.substr($0, 0, -7).'FHEM/00_SONOS.pm '.$port.' '.$verboselevel.' '.(($attr{global}{mseclog}) ? '1' : '0')); + exit(0); + } + } else { + $socket->sockopt(SO_LINGER, pack("ii", 1, 0)); + + # Antwort vom Client weglesen... + my $answer; + $socket->recv($answer, 50); + + # Hiermit wird eine etwaig bestehende Thread-Struktur beendet und diese Verbindung selbst geschlossen... + eval{ + $socket->send("disconnect\n", 0); + $socket->shutdown(2); + $socket->close(); + }; + } + + return undef; +} + +######################################################################################## +# +# SONOS_InitClientProcessLater - Initializes the client-process at a later time +# +# Parameter hash = The device-hash +# +######################################################################################## +sub SONOS_InitClientProcessLater($) { + my ($hash) = @_; + + # Begrüßung weglesen... + my $answer = DevIo_SimpleRead($hash); + + # Verbindung aufbauen... + InternalTimer(gettimeofday() + 1, 'SONOS_InitClientProcess', $hash, 0); + + return undef; +} + +######################################################################################## +# +# SONOS_InitClientProcess - Initializes the client-process +# +# Parameter hash = The device-hash +# +######################################################################################## +sub SONOS_InitClientProcess($) { + my ($hash) = @_; + + my @playerudn = (); + my @playername = (); + foreach my $fhem_dev (sort keys %main::defs) { + next if($main::defs{$fhem_dev}{TYPE} ne 'SONOSPLAYER'); + + push @playerudn, $main::defs{$fhem_dev}{UDN}; + push @playername, $main::defs{$fhem_dev}{NAME}; + } + + # Grundsätzliche Informationen bzgl. der konfigurierten Player übertragen... + my $setDataString = 'SetData:'.$hash->{NAME}.':'.AttrVal($hash->{NAME}, 'verbose', '3').':'.AttrVal($hash->{NAME}, 'pingType', 'none').':'.join(',', @playername).':'.join(',', @playerudn); + SONOS_Log undef, 5, $setDataString; + DevIo_SimpleWrite($hash, $setDataString."\n", 0); + + # Gemeldete Attribute, Definitionen und Readings übertragen... + foreach my $fhem_dev (sort keys %main::defs) { + if (($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER') || ($main::defs{$fhem_dev}{TYPE} eq 'SONOS')) { + # Den Namen des Devices ermitteln (normalerweise die UDN, bis auf das zentrale Sonos-Device) + my $dataName; + if ($main::defs{$fhem_dev}{TYPE} eq 'SONOS') { + $dataName = 'SONOS'; + } else { + $dataName = $main::defs{$fhem_dev}{UDN}; + } + + # Variable für die gesammelten Informationen, die übertragen werden sollen... + my %valueList = (); + + # Attribute + foreach my $key (keys %{$main::attr{$fhem_dev}}) { + if (SONOS_posInList($key, @SONOS_PossibleAttributes) != -1) { + $valueList{$key} = $main::attr{$fhem_dev}{$key}; + } + } + + # Definitionen + foreach my $key (keys %{$main::defs{$fhem_dev}}) { + if (SONOS_posInList($key, @SONOS_PossibleDefinitions) != -1) { + $valueList{$key} = $main::defs{$fhem_dev}{$key}; + } + } + + # Readings + foreach my $key (keys %{$main::defs{$fhem_dev}{READINGS}}) { + if (SONOS_posInList($key, @SONOS_PossibleReadings) != -1) { + $valueList{$key} = $main::defs{$fhem_dev}{READINGS}{$key}{VAL}; + } + } + + # Werte in Text-Array umwandeln und dabei prüfen, ob überhaupt ein Wert gesetzt werden soll... + my @values = (); + foreach my $key (keys %valueList) { + if (defined($key) && defined($valueList{$key})) { + push @values, $key.'='.uri_escape($valueList{$key}); + } + } + + # Übertragen... + SONOS_Log undef, 5, 'SetValues:'.$dataName.':'.join('|', @values); + DevIo_SimpleWrite($hash, 'SetValues:'.$dataName.':'.join('|', @values)."\n", 0); + } + } + + # Alle Informationen sind drüben, dann Threads dort drüben starten + DevIo_SimpleWrite($hash, "StartThread\n", 0); + + # Interner Timer für die Überprüfung der Verbindung zum Client (nicht verwechseln mit dem IsAlive-Timer, der die Existenz eines Sonosplayers überprüft) + InternalTimer(gettimeofday() + ($hash->{INTERVAL} * 2), 'SONOS_IsSubprocessAliveChecker', $hash, 0); + + return undef; +} + +######################################################################################## +# +# SONOS_IsSubprocessAliveChecker - Internal checking routine for isAlive of the subprocess +# +######################################################################################## +sub SONOS_IsSubprocessAliveChecker() { + my ($hash) = @_; + + my $answer; + my $socket = new IO::Socket::INET(PeerAddr => $hash->{DeviceName}, Proto => 'tcp'); + if ($socket) { + $socket->sockopt(SO_LINGER, pack("ii", 1, 0)); + + $socket->recv($answer, 500); + + $socket->send("hello\n", 0); + $socket->recv($answer, 500); + + $socket->send("goaway\n", 0); + + $socket->shutdown(2); + $socket->close(); + } + + if (defined($answer)) { + $answer =~ s/[\r\n]//g; + } + + if (!defined($answer) || ($answer ne 'OK')) { + SONOS_Log undef, 0, 'No Answer from Subprocess. Restart Sonos-Subprocess...'; + + # Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden... + RemoveInternalTimer($hash); + DevIo_SimpleWrite($hash, "disconnect\n", 0); + DevIo_CloseDev($hash); + + # Neu anstarten... + SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer); + InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0); + } elsif (defined($answer) && ($answer eq 'OK')) { + SONOS_Log undef, 4, 'Got correct Answer from Subprocess...'; + + InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0); + } +} + +######################################################################################## +# +# SONOS_DoTriggerInternal - Internal working routine for DoTrigger and PeekTriggerQueueInLocalThread +# +######################################################################################## +sub SONOS_DoTriggerInternal($$) { + my ($triggerType, @lines) = @_; + + # Eval Kommandos ausführen + my %doTriggerHashParam; + my @doTriggerArrayParam; + my $doTriggerScalarParam; + foreach my $line (@lines) { + my $reftype = reftype $line; + + if (!defined $reftype) { + SONOS_Log undef, 5, $triggerType.'Trigger()-Line: '.$line; + + eval $line; + if ($@) { + SONOS_Log undef, 2, 'Error during '.$triggerType.'Trigger: '.$@.' - Trying to execute \''.$line.'\''; + } + + undef(%doTriggerHashParam); + undef(@doTriggerArrayParam); + undef($doTriggerScalarParam); + } elsif($reftype eq 'HASH') { + %doTriggerHashParam = %{$line}; + SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerHashParam: '.SONOS_Stringify(\%doTriggerHashParam); + } elsif($reftype eq 'ARRAY') { + @doTriggerArrayParam = @{$line}; + SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerArrayParam: '.SONOS_Stringify(\@doTriggerArrayParam); + } elsif($reftype eq 'SCALAR') { + $doTriggerScalarParam = ${$line}; + SONOS_Log undef, 5, $triggerType.'Trigger()-doTriggerScalarParam: '.SONOS_Stringify(\$doTriggerScalarParam); + } + } +} + +######################################################################################## +# +# SONOS_Get - Implements GetFn function +# +# Parameter hash = hash of the master +# a = argument array +# +######################################################################################## +sub SONOS_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + + # check argument + return "SONOS: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) if(!defined($gets{$reading})); + + # some argument needs parameter(s), some not + return "SONOS: $a[1] needs parameter(s): ".$gets{$a[1]} if (scalar(split(',', $gets{$a[1]})) > scalar(@a) - 2); + + # getter + if (lc($reading) eq 'groups') { + return SONOS_ConvertZoneGroupStateToString(SONOS_ConvertZoneGroupState(ReadingsVal($name, 'ZoneGroupState', ''))); + } + + return undef; +} + +######################################################################################## +# +# SONOS_ConvertZoneGroupState - Retrieves the Groupstate in an array (Elements are UDNs) +# +######################################################################################## +sub SONOS_ConvertZoneGroupState($) { + my ($zoneGroupState) = @_; + + my @groups = (); + while ($zoneGroupState =~ m/(.*?)<\/ZoneGroup>/gi) { + my @group = ($1.'_MR'); + my $groupMember = $2; + + while ($groupMember =~ m//gi) { + my $udn = $1; + my $string = $2; + push @group, $udn.'_MR' if (!($string =~ m/IsZoneBridge="."/) && !SONOS_isInList($udn.'_MR', @group)); + + # Etwaig von vorher enthaltene Bridges wieder entfernen (wenn sie bereits als Koordinator eingesetzt wurde) + if ($string =~ m/IsZoneBridge="."/) { + for(my $i = 0; $i <= $#group; $i++) { + delete $group[$i] if ($group[$i] eq $udn.'_MR'); + } + } + } + + # Die Abspielgruppe hinzufügen, wenn sie nicht leer ist (kann bei Bridges passieren) + push @groups, \@group if ($#group >= 0); + } + + return @groups; +} + +######################################################################################## +# +# SONOS_ConvertZoneGroupStateToString - Converts the GroupState into a String +# +######################################################################################## +sub SONOS_ConvertZoneGroupStateToString($) { + my (@groups) = @_; + + # UDNs durch Devicenamen ersetzen und dabei gleich das Ergebnis zusammenbauen + my $result = ''; + foreach my $gelem (@groups) { + $result .= '['; + foreach my $elem (@{$gelem}) { + $elem = SONOS_getSonosPlayerByUDN($elem)->{NAME}; + } + $result .= join(', ', @{$gelem}).'], '; + } + + return substr($result, 0, -2); +} + +######################################################################################## +# +# SONOS_Set - Implements SetFn function +# +# Parameter hash +# a = argument array +# +######################################################################################## +sub SONOS_Set($@) { + my ($hash, @a) = @_; + + # %setCopy enthält eine Kopie von %sets, da für eine ?-Anfrage u.U. ein Slider zurückgegeben werden muss... + my %setcopy; + if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) { + foreach my $key (keys %sets) { + my $oldkey = $key; + $key = $key.':slider,0,1,100' if ($key eq 'Volume'); + $key = $key.':slider,-100,1,100' if ($key eq 'Balance'); + + $setcopy{$key} = $sets{$oldkey}; + } + } else { + %setcopy = %sets; + } + + # for the ?-selector: which values are possible + return join(" ", sort keys %setcopy) if($a[1] eq '?'); + + # check argument + return "SONOS: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) if(!defined($sets{$a[1]})); + + # some argument needs parameter(s), some not + return "SONOS: $a[1] needs parameter(s): ".$sets{$a[1]} if (scalar(split(',', $sets{$a[1]})) > scalar(@a) - 2); + + # define vars + my $key = $a[1]; + my $value = $a[2]; + my $value2 = $a[3]; + my $name = $hash->{NAME}; + + # setter + if (lc($key) eq 'groups') { + # [Sonos_Jim], [Sonos_Wohnzimmer, Sonos_Schlafzimmer] => [] Liste, Der erste Eintrag soll Koordinator sein + # Idee: [Sonos_Jim], {Sonos_Wohnzimmer, Sonos_Schlafzimmer} => {} Menge, bedeutet beliebiger Koordinator + + my $text = ''; + for(my $i = 2; $i < @a; $i++) { + $text .= ' '.$a[$i]; + } + $text =~ s/ //g; + + # Aktuellen Zustand holen + my @current; + my $current = SONOS_Get($hash, qw($hash->{NAME} Groups)); + $current =~ s/ //g; + while ($current =~ m/(\[.*?\])/ig) { + my @tmp = split(/,/, substr($1, 1, -1)); + push @current, \@tmp; + } + + # Gewünschten Zustand holen + my @desiredList; + my @desiredCrowd; + while ($text =~ m/([\[\{].*?[\}\]])/ig) { + my @tmp = split(/,/, substr($1, 1, -1)); + if (substr($1, 0, 1) eq '{') { + push @desiredCrowd, \@tmp; + } else { + push @desiredList, \@tmp; + } + } + SONOS_Log undef, 5, "Desired-Crowd: ".Dumper(\@desiredCrowd); + SONOS_Log undef, 5, "Desired-List: ".Dumper(\@desiredList); + + # Erstmal die Listen sicherstellen + foreach my $dElem (@desiredList) { + my @list = @{$dElem}; + for(my $i = 0; $i <= $#list; $i++) { # Die jeweilige Desired-List + my $elem = $list[$i]; + my $elemHash = SONOS_getDeviceDefHash($elem); + my $reftype = reftype $elemHash; + if (!defined($reftype) || $reftype ne 'HASH') { + SONOS_Log undef, 5, "Hash not found for Device '$elem'. Is it gone away or not known?"; + return undef; + } + + # Das Element soll ein Gruppenkoordinator sein + if ($i == 0) { + my $cPos = -1; + foreach my $cElem (@current) { + $cPos = SONOS_posInList($elem, @{$cElem}); + last if ($cPos != -1); + } + + # Ist es aber nicht... also erstmal dazu machen + if ($cPos != 0) { + SONOS_DoWork($elemHash->{UDN}, 'makeStandaloneGroup'); + usleep(250_000); + } + } else { + # Alle weiteren dazufügen + my $cHash = SONOS_getDeviceDefHash($list[0]); + SONOS_DoWork($cHash->{UDN}, 'addMember', $elemHash->{UDN}); + usleep(250_000); + } + } + } + + # Jetzt noch die Mengen sicherstellen + # Dazu aktuellen Zustand nochmal holen + #@current = (); + #$current = SONOS_Get($hash, qw($hash->{NAME} Groups)); + #$current =~ s/ //g; + #while ($current =~ m/(\[.*?\])/ig) { + # my @tmp = split(/,/, substr($1, 1, -1)); + # push @current, \@tmp; + #} + #SONOS_Log undef, 5, "Current after List: ".Dumper(\@current); + + } elsif (lc($key) =~ m/(Stop|Pause)All/i) { + my $commandType = $1; + + # Aktuellen Zustand holen + my @current; + my $current = SONOS_Get($hash, qw($hash->{NAME} Groups)); + $current =~ s/ //g; + while ($current =~ m/(\[.*?\])/ig) { + my @tmp = split(/,/, substr($1, 1, -1)); + push @current, \@tmp; + } + + # Alle Gruppenkoordinatoren zum Stoppen/Pausieren aufrufen + foreach my $cElem (@current) { + my @currentElem = @{$cElem}; + SONOS_DoWork(SONOS_getDeviceDefHash($currentElem[0])->{UDN}, lc($commandType), 0); + } + } else { + return 'Not implemented yet!'; + } + + return (undef, 1); +} + +######################################################################################## +# +# SONOS_DoWork - Communicates with the forked Part via Telnet and over there via ComObjectTransportQueue +# +# Parameter deviceName = Devicename of the SonosPlayer +# method = Name der "Methode" die im Thread-Context ausgeführt werden soll +# params = Parameter for the method +# +######################################################################################## +sub SONOS_DoWork($@) { + my ($udn, $method, @params) = @_; + + if (!defined($udn)) { + SONOS_Log undef, 0, "ERROR in DoWork: '$method' -> UDN is undefined - ".Dumper(\@params); + } + + # Etwaige optionale Parameter, die sonst undefined wären, löschen + for(my $i = 0; $i <= $#params; $i++) { + if (!defined($params[$i])) { + delete($params[$i]); + } + } + + my $hash = SONOS_getDeviceDefHash(undef); + + DevIo_SimpleWrite($hash, 'DoWork:'.$udn.':'.$method.':'.join(',', @params)."\r\n", 0); + + return undef; +} + +######################################################################################## +# +# SONOS_Discover - Discover SonosPlayer, +# indirectly autocreate devices if not already present (via callback) +# +######################################################################################## +sub SONOS_Discover() { + SONOS_Log undef, 3, 'UPnP-Thread gestartet.'; + + $SIG{'PIPE'} = 'IGNORE'; + $SIG{'CHLD'} = 'IGNORE'; + + # Thread 'cancellation' signal handler + $SIG{'INT'} = sub { + # Sendeliste leeren + while ($SONOS_Client_SendQueue->pending()) { + $SONOS_Client_SendQueue->dequeue(); + } + + # Empfängerliste leeren + while ($SONOS_ComObjectTransportQueue->pending()) { + $SONOS_ComObjectTransportQueue->dequeue(); + } + + # UPnP-Listener beenden + SONOS_StopControlPoint(); + + SONOS_Log undef, 3, 'Controlpoint-Listener wurde beendet.'; + return 1; + }; + + # Thread Signal Handler for doing some work in this thread 'environment' + $SIG{'HUP'} = sub { + while ($SONOS_ComObjectTransportQueue->pending()) { + my $data = $SONOS_ComObjectTransportQueue->peek(); + my $workType = $data->{WorkType}; + my $udn = $data->{UDN}; + my @params = @{$data->{Params}}; + + eval { + if ($workType eq 'setVerbose') { + $SONOS_Client_LogLevel = $params[0]; + SONOS_Log undef, 0, "Setting LogLevel to new value: $SONOS_Client_LogLevel"; + } elsif ($workType eq 'setName') { + my $value1 = SONOS_Utf8ToLatin1($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes($value1, '', ''))); + } + } elsif ($workType eq 'setIcon') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_DevicePropertiesProxy{$udn}->SetZoneAttributes('', 'x-rincon-roomicon:'.$value1, ''))); + } + } elsif ($workType eq 'getCurrentTrackPosition') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime')); + } + } elsif ($workType eq 'setCurrentTrackPosition') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'REL_TIME', $value1); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime')); + } + } elsif ($workType eq 'reportUnresponsiveDevice') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_ZoneGroupTopologyProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_ZoneGroupTopologyProxy{$udn}->ReportUnresponsiveDevice($value1, 'VerifyThenRemoveSystemwide'))); + } + } elsif ($workType eq 'setGroupVolume') { + my $value1 = $params[0]; + my $value2 = $params[1]; + + # Wenn ein fixer Wert für alle Gruppenmitglieder gleich gesetzt werden soll... + if (defined($value2) && lc($value2) eq 'fixed') { + + } else { + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume')); + } + } + } elsif ($workType eq 'setSnapshotGroupVolume') { + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0))); + } + } elsif ($workType eq 'setVolume') { + my $value1 = $params[0]; + my $ramptype = $params[1]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + if (defined($ramptype)) { + if ($ramptype == 1) { + $ramptype = 'SLEEP_TIMER_RAMP_TYPE'; + } elsif ($ramptype == 2) { + $ramptype = 'AUTOPLAY_RAMP_TYPE'; + } elsif ($ramptype == 3) { + $ramptype = 'ALARM_RAMP_TYPE'; + } + my $ramptime = $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, '')->getValue('RampTime'); + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started'); + } else { + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume')); + } + } + } elsif ($workType eq 'setRelativeGroupVolume') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $value1)->getValue('NewVolume')); + } + } elsif ($workType eq 'setRelativeVolume') { + my $value1 = $params[0]; + my $ramptype = $params[1]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + if (defined($ramptype)) { + if ($ramptype == 1) { + $ramptype = 'SLEEP_TIMER_RAMP_TYPE'; + } elsif ($ramptype == 2) { + $ramptype = 'AUTOPLAY_RAMP_TYPE'; + } elsif ($ramptype == 3) { + $ramptype = 'ALARM_RAMP_TYPE'; + } + + # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen + if ($value1 =~ m/([+-])(\d+)\%/) { + my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'); + $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) }; + } else { + # Hier aus der Relativangabe eine Absolutangabe für den Aufruf von RampToVolume machen + $value1 = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume') + $value1; + } + $SONOS_RenderingControlProxy{$udn}->RampToVolume(0, 'Master', $ramptype, $value1, 0, ''); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Ramp to '.$value1.' with Type '.$params[1].' started'); + } else { + # Wenn eine Prozentangabe übergeben wurde, dann die wirkliche Ziellautstärke ermitteln/berechnen + if ($value1 =~ m/([+-])(\d+)\%/) { + my $currentValue = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'); + $value1 = $currentValue + eval{ $1.($currentValue * ($2 / 100)) }; + + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume')); + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->SetRelativeVolume(0, 'Master', $value1)->getValue('NewVolume')); + } + } + } + } elsif ($workType eq 'setBalance') { + my $value1 = $params[0]; + + # Balancewert auf die beiden Lautstärkeseiten aufteilen... + my $volumeLeft = 100; + my $volumeRight = 100; + if ($value1 < 0) { + $volumeRight = 100 + $value1; + } else { + $volumeLeft = 100 - $value1; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'LF', $volumeLeft); + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'RF', $volumeRight); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume'); + $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume'); + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.((-$volumeLeft) + $volumeRight)); + } + } elsif ($workType eq 'setLoudness') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + $SONOS_RenderingControlProxy{$udn}->SetLoudness(0, 'Master', SONOS_ConvertWordToNum($value1)); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetLoudness(0, 'Master')->getValue('CurrentLoudness'))); + } + } elsif ($workType eq 'setBass') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + $SONOS_RenderingControlProxy{$udn}->SetBass(0, $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetBass(0)->getValue('CurrentBass')); + } + } elsif ($workType eq 'setTreble') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + $SONOS_RenderingControlProxy{$udn}->SetTreble(0, $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_RenderingControlProxy{$udn}->GetTreble(0)->getValue('CurrentTreble')); + } + } elsif ($workType eq 'setMute') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1)); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute'))); + } + } elsif ($workType eq 'setMuteT') { + my $value1 = 'off'; + if (SONOS_CheckProxyObject($udn, $SONOS_RenderingControlProxy{$udn})) { + if ($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute') == 0) { + $value1 = 'on'; + } else { + $value1 = 'off'; + } + + $SONOS_RenderingControlProxy{$udn}->SetMute(0, 'Master', SONOS_ConvertWordToNum($value1)); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_RenderingControlProxy{$udn}->GetMute(0, 'Master')->getValue('CurrentMute'))); + } + } elsif ($workType eq 'setGroupMute') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, SONOS_ConvertWordToNum($value1)); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute'))); + } + } elsif ($workType eq 'setShuffle') { + my $value1 = SONOS_ConvertWordToNum($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode'); + + my $shuffle = $result eq 'SHUFFLE' || $result eq 'SHUFFLE_NOREPEAT'; + my $repeat = $result eq 'SHUFFLE' || $result eq 'REPEAT_ALL'; + + my $newMode = 'NORMAL'; + $newMode = 'SHUFFLE' if ($value1 && $repeat); + $newMode = 'SHUFFLE_NOREPEAT' if ($value1 && !$repeat); + $newMode = 'REPEAT_ALL' if (!$value1 && $repeat); + + $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, $newMode); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode'); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($result eq 'SHUFFLE' || $result eq 'SHUFFLE_NOREPEAT')); + } + } elsif ($workType eq 'setRepeat') { + my $value1 = SONOS_ConvertWordToNum($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode'); + + my $shuffle = $result eq 'SHUFFLE' || $result eq 'SHUFFLE_NOREPEAT'; + my $repeat = $result eq 'SHUFFLE' || $result eq 'REPEAT_ALL'; + + my $newMode = 'NORMAL'; + $newMode = 'SHUFFLE' if ($value1 && $shuffle); + $newMode = 'SHUFFLE_NOREPEAT' if (!$value1 && $shuffle); + $newMode = 'REPEAT_ALL' if ($value1 && !$shuffle); + + $SONOS_AVTransportControlProxy{$udn}->SetPlayMode(0, $newMode); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportSettings(0)->getValue('PlayMode'); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($result eq 'SHUFFLE' || $result eq 'REPEAT_ALL')); + } + } elsif ($workType eq 'setCrossfadeMode') { + my $value1 = SONOS_ConvertWordToNum($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + $SONOS_AVTransportControlProxy{$udn}->SetCrossfadeMode(0, $value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_AVTransportControlProxy{$udn}->GetCrossfadeMode(0)->getValue('CrossfadeMode'))); + } + } elsif ($workType eq 'setLEDState') { + my $value1 = (SONOS_ConvertWordToNum($params[0])) ? 'On' : 'Off'; + + if (SONOS_CheckProxyObject($udn, $SONOS_DevicePropertiesProxy{$udn})) { + $SONOS_DevicePropertiesProxy{$udn}->SetLEDState($value1); + + # Wert wieder abholen, um das wahre Ergebnis anzeigen zu können + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_ConvertNumToWord($SONOS_DevicePropertiesProxy{$udn}->GetLEDState()->getValue('CurrentLEDState'))); + } + } elsif ($workType eq 'play') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1))); + } + } elsif ($workType eq 'stop') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Stop(0))); + } + } elsif ($workType eq 'pause') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Pause(0))); + } + } elsif ($workType eq 'previous') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Previous(0))); + } + } elsif ($workType eq 'next') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Next(0))); + } + } elsif ($workType eq 'setTrack') { + my $value1 = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + # Abspielliste aktivieren? + my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI'); + if ($currentURI !~ m/x-rincon-queue:/) { + my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, ''); + my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), ''); + } + + if (lc($value1) eq 'random') { + $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', int(rand($SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('NrTracks')))); + } else { + $SONOS_AVTransportControlProxy{$udn}->Seek(0, 'TRACK_NR', $value1); + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track')); + } + } elsif ($workType eq 'setCurrentPlaylist') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + # Abspielliste aktivieren? + my $currentURI = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0)->getValue('CurrentURI'); + if ($currentURI !~ m/x-rincon-queue:/) { + my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, ''); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), ''))); + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Not neccessary!'); + } + } + } elsif ($workType eq 'getPlaylists') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?<\/container>/ig) { + $resultHash{$1} = $2; + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"'); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'getPlaylistsWithCovers') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?(.*?)<\/res>.*?<\/container>/ig) { + $resultHash{$1}->{Title} = $2; + $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3); + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.Dumper(\%resultHash)); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'getFavourites') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?<\/item>/ig) { + $resultHash{$1} = $2; + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"'); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'getFavouritesWithCovers') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?(.*?)<\/res>.*?<\/item>/ig) { + $resultHash{$1}->{Title} = $2; + $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3); + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.Dumper(\%resultHash)); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'getRadios') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?(.*?)<\/res>.*?<\/item>/ig) { + $resultHash{$1} = $2; + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"'); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'getRadiosWithCovers') { + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?(.*?)<\/res>.*?<\/item>/ig) { + $resultHash{$1}->{Title} = $2; + $resultHash{$1}->{Cover} = SONOS_MakeCoverURL($udn, $3); + } + + $Data::Dumper::Indent = 0; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.Dumper(\%resultHash)); + $Data::Dumper::Indent = 2; + } + } elsif ($workType eq 'loadRadio') { + my $radioName = uri_unescape($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('R:0/0', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + SONOS_Log $udn, 5, 'LoadRadio BrowseResult: '.$tmp; + + my %resultHash; + while ($tmp =~ m/()(.*?)<\/dc:title>.*?(.*?<\/upnp:class>).*?(.*?)<\/res>.*?<\/item>/ig) { + $resultHash{$3}{TITLE} = $3; + $resultHash{$3}{RES} = decode_entities($5); + $resultHash{$3}{METADATA} = $SONOS_DIDLHeader.$1.''.$3.''.$4.'SA_RINCON65031_'.$SONOS_DIDLFooter; + } + + if (!$resultHash{$radioName}) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Radio "'.$radioName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"'); + return; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_Log $udn, 5, 'LoadRadio SetAVTransport-Res: "'.$resultHash{$radioName}{RES}.'", -Meta: "'.$resultHash{$radioName}{METADATA}.'"'; + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$radioName}{RES}, $resultHash{$radioName}{METADATA}))); + } + } + } elsif ($workType eq 'startFavourite') { + my $favouriteName = uri_unescape($params[0]); + my $nostart = 0; + if (defined($params[1]) && lc($params[1]) eq 'nostart') { + $nostart = 1; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('FV:2', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + SONOS_Log $udn, 5, 'StartFavourite BrowseResult: '.$tmp; + + my %resultHash; + while ($tmp =~ m/()(.*?)<\/dc:title>.*?(.*?)<\/res>.*?(.*?)<\/r:resMD>.*?<\/item>/ig) { + $resultHash{$3}{TITLE} = $3; + $resultHash{$3}{RES} = decode_entities($4); + $resultHash{$3}{METADATA} = decode_entities($5); + } + + if (!$resultHash{$favouriteName}) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Favourite "'.$favouriteName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"'); + return; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann + if ($resultHash{$favouriteName}{METADATA} =~ m/object.container.playlistContainer<\/upnp:class>/i) { + + SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"'; + + # Queue leeren + $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0); + + # Queue wieder füllen + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, 0, 1))); + + # Queue aktivieren + $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), ''); + } else { + SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"'; + + # Stück aktivieren + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}))); + } + + # Abspielen starten, wenn nicht absichtlich verhindert + $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart); + } + } + } elsif ($workType eq 'loadPlaylist') { + my $answer = ''; + my $playlistName = uri_unescape($params[0]); + my $overwrite = $params[1]; + + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + # Queue vorher leeren? + if ($overwrite) { + $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(); + $answer .= 'Queue successfully emptied. '; + } + + my $currentInsertPos = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track') + 1; + + if ($playlistName =~ /^:m3ufile:(.*)/) { + my @URIs = (); + my @Metas = (); + + # Versuche die Datei zu öffnen + if (!open(FILE, '<'.$1)) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during opening file "'.$1.'": '.$!); + return; + }; + + binmode(FILE, ':encoding(utf-8)'); + while () { + if ($_ =~ m/^ *([^#].*) *\n/) { + next if ($1 eq ''); + + my ($res, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($1)); + + push(@URIs, $res); + push(@Metas, $meta); + } + } + close FILE; + + my $sliceSize = 16; + my $result; + my $count = 0; + + SONOS_Log $udn, 5, "Start-Adding: Count ".scalar(@URIs)." / $sliceSize"; + + for my $i (0..int(scalar(@URIs) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren + my $startIndex = $i * $sliceSize; + my $endIndex = $startIndex + $sliceSize - 1; + $endIndex = SONOS_Min(scalar(@URIs) - 1, $endIndex); + + SONOS_Log $udn, 5, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)"; + SONOS_Log $udn, 5, "Upload($currentInsertPos)-URI: ".join(' ', @URIs[$startIndex..$endIndex]); + SONOS_Log $udn, 5, "Upload($currentInsertPos)-Meta: ".join(' ', @Metas[$startIndex..$endIndex]); + + $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, join(' ', @URIs[$startIndex..$endIndex]), join(' ', @Metas[$startIndex..$endIndex]), '', '', $currentInsertPos, 0); + if (!$result->isSuccessful()) { + $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' '; + } + + $currentInsertPos += $endIndex - $startIndex + 1; + $count = $endIndex + 1; + } + + if ($result->isSuccessful()) { + $answer .= 'Added '.$count.' entries from file "'.$1.'". There are now '.$result->getValue('NewQueueLength').' entries in Queue. '; + } else { + $answer .= 'Adding: '.SONOS_UPnPAnswerMessage($result).' '; + } + } else { + my $browseResult = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $browseResult->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?<\/container>/ig) { + $resultHash{$2} = $1; + } + + if (!$resultHash{$playlistName}) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"'); + return; + } + + # Titel laden + my $playlistData = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($resultHash{$playlistName}, 'BrowseMetadata', '', 0, 0, ''); + my $playlistRes = SONOS_GetTagData('res', $playlistData->getValue('Result')); + + # Elemente an die Queue anhängen + my $result = $SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $playlistRes, '', $currentInsertPos, 0); + $answer .= $result->getValue('NumTracksAdded').' Elems added. '.$result->getValue('NewQueueLength').' Elems in list now. '; + } + + # Die Liste als aktuelles Abspielstück einstellen + my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, ''); + my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), ''); + $answer .= 'Startlist: '.SONOS_UPnPAnswerMessage($result).'. '; + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$answer); + } + } elsif ($workType eq 'setAlarm') { + my $create = $params[0]; + my $id = $params[1]; + + # Alle folgenden Parameter weglesen und an den letzten Parameter anhängen + my $values = {}; + my $val = join(',', @params[2..$#params]); + if ($val ne '') { + SONOS_Log $udn, 5, 'Val: '.$val; + $values = \%{eval($val)}; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) { + my @idList = split(',', SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListIDs', '')); + + # Die Room-ID immer fest auf den aktuellen Player eintragen. + # Hiermit sollte es nicht mehr möglich sein, einen Alarm für einen anderen Player einzutragen. Das kann man auch direkt an dem anderen Player durchführen... + $values->{RoomUUID} = $1 if ($udn =~ m/(.*?)_MR/i); + + if (lc($create) eq 'update') { + if (!SONOS_isInList($id, @idList)) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0)); + } else { + my %alarm = %{eval(SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmList', '{}'))->{$id}}; + + # Replace old values with the given new ones... + for my $key (keys %alarm) { + if (defined($values->{$key})) { + $alarm{$key} = $values->{$key}; + } + } + + if (!SONOS_CheckAndCorrectAlarmHash(\%alarm)) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0)); + } else { + # Send to Zoneplayer + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->UpdateAlarm($id, $alarm{StartTime}, $alarm{Duration}, $alarm{Recurrence}, $alarm{Enabled}, $alarm{RoomUUID}, $alarm{ProgramURI}, $alarm{ProgramMetaData}, $alarm{PlayMode}, $alarm{Volume}, $alarm{IncludeLinkedZones}))); + } + } + } elsif (lc($create) eq 'create') { + # Check if all parameters are given + if (!SONOS_CheckAndCorrectAlarmHash($values)) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0)); + } else { + # create here on Zoneplayer + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.$SONOS_AlarmClockControlProxy{$udn}->CreateAlarm($values->{StartTime}, $values->{Duration}, $values->{Recurrence}, $values->{Enabled}, $values->{RoomUUID}, $values->{ProgramURI}, $values->{ProgramMetaData}, $values->{PlayMode}, $values->{Volume}, $values->{IncludeLinkedZones})->getValue('AssignedID')); + } + } elsif (lc($create) eq 'delete') { + if (!SONOS_isInList($id, @idList)) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0).' ID is incorrect!'); + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->DestroyAlarm($id))); + } + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0)); + } + } + } elsif ($workType eq 'setDailyIndexRefreshTime') { + my $time = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AlarmClockControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AlarmClockControlProxy{$udn}->SetDailyIndexRefreshTime($time))); + } + } elsif ($workType eq 'setSleepTimer') { + my $time = $params[0]; + + if ((lc($time) eq 'off') || ($time =~ /0+:0+:0+/)) { + $time = ''; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->ConfigureSleepTimer(0, $time))); + } + } elsif ($workType eq 'addMember') { + my $memberudn = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$memberudn}) && SONOS_CheckProxyObject($udn, $SONOS_ZoneGroupTopologyProxy{$memberudn})) { + # Wenn der hinzuzufügende Player Koordinator einer anderen Gruppe ist, + # dann erst mal ein anderes Gruppenmitglied zum Koordinator machen + my @zoneTopology = SONOS_ConvertZoneGroupState($SONOS_ZoneGroupTopologyProxy{$memberudn}->GetZoneGroupState()->getValue('ZoneGroupState')); + + # Hier fehlt noch die Umstellung der bestehenden Gruppe... + + # Sicherstellen, dass der hinzuzufügende Player kein Bestandteil einer Gruppe mehr ist. + $SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0); + + my $coordinatorUDNShort = $1 if ($udn =~ m/(.*)_MR/); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->SetAVTransportURI(0, 'x-rincon:'.$coordinatorUDNShort, ''))); + } + } elsif ($workType eq 'removeMember') { + my $memberudn = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$memberudn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$memberudn}->BecomeCoordinatorOfStandaloneGroup(0))); + } + } elsif ($workType eq 'makeStandaloneGroup') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->BecomeCoordinatorOfStandaloneGroup(0))); + } + } elsif ($workType eq 'emptyPlaylist') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue())); + } + } elsif ($workType eq 'savePlaylist') { + my $playlistName = $params[0]; + my $playlistType = $params[1]; + + $playlistName =~s/ $//g; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + if ($playlistType eq ':m3ufile:') { + open (FILE, '>'.$playlistName); + print FILE "#EXTM3U\n"; + + my $startIndex = 0; + my $result; + my $count = 0; + do { + $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, ''); + my $queueSongdata = $result->getValue('Result'); + + while ($queueSongdata =~ m/(.*?)<\/item>/gi) { + my $item = $1; + my $res = uri_unescape(SONOS_GetURIFromQueueValue(decode_entities($1))) if ($item =~ m/(.*?)<\/res>/i); + my $artist = decode_entities($1) if ($item =~ m/(.*?)<\/dc:creator>/i); + my $title = decode_entities($1) if ($item =~ m/(.*?)<\/dc:title>/i); + my $time = 0; + $time = SONOS_GetTimeSeconds($1) if ($item =~ m/.*?duration="(.*?)"/); + + # In Datei wegschreiben + eval { + print FILE "#EXTINF:$time,($artist) $title\n$res\n"; + }; + $count++; + } + + $startIndex += $result->getValue('NumberReturned'); + } while ($startIndex < $result->getValue('TotalMatches')); + + + close FILE; + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New M3U-File "'.$playlistName.'" successfully created with '.$count.' entries!'); + } else { + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?<\/container>/ig) { + $resultHash{$2} = $1; + } + + if ($resultHash{$playlistName}) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Existing Playlist "'.$playlistName.'" updated: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, $resultHash{$playlistName}))); + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': New Playlist '.$playlistName.' created: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SaveQueue(0, $playlistName, ''))); + } + } + } + } elsif ($workType eq 'createThemeList') { + # set Player CreateThemeList [ ] [ShuffleList] [EmptyList] [Play] + # set Player CreateThemeList ARTIST=*{1} EmptyList Play + # set Player CreateThemeList ARTIST=Herbert%20Grönemeyer ShuffleList EmptyList Play + # set Player CreateThemeList ARTIST=Herbert%20Grönemeyer ALBUM=Zwölf ShuffleList EmptyList Play + # ARTIST, ALBUMARTIST, ALBUM, GENRE, COMPOSER, TRACKS + # SearchValue: * -> Beliebiger Wert, {N} -> Anzahl einschränken + + my $shuffleList = 0; + my $emptyList = 0; + my $play = 0; + my %searches; + + my $answer = ''; + + #while ($SONOS_ComObjectTransportQueue->pending() > 0) { + # my $tmp = $SONOS_ComObjectTransportQueue->dequeue(); + # + # if ($tmp =~ /ShuffleList/i) { + # $shuffleList = 1; + # } elsif ($tmp =~ /EmptyList/i) { + # $emptyList = 1; + # } elsif ($tmp =~ /Play/i) { + # $play = 1; + # } elsif ($tmp =~ /(.*?)=(.*?)/) { + # $searches{$1} = $2; + # } else { + # SONOS_Log $udn, 1, 'Error during parsing of CreateThemeList-Parameter: "'.$tmp.'". Ignoring it!'; + # } + #} + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn}) && SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + # EmptyList before adding new elements + if ($emptyList) { + $answer .= ', EmptyList: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue()); + } + + # Search and Load + + # Shuffle retrieved list + if ($shuffleList) { + # Do shuffeling here + + $answer .= ', ShuffleList: '.SONOS_UPnPAnswerMessage(0); + } + + # Die Liste als aktuelles Abspielstück einstellen + my $queueMetadata = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, ''); + my $result = $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), ''); + $answer .= ', Startlist: '.SONOS_UPnPAnswerMessage($result); + + # Play afterwards? + if ($play) { + $answer .= ', Play: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)); + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.substr($answer, 2)); # Das führende Komma wieder entfernen + } + } elsif ($workType eq 'deleteProxyObjects') { + # Wird vom Sonos-Device selber in IsAlive benötigt + SONOS_DeleteProxyObjects($udn); + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(1)); + } elsif ($workType eq 'renewSubscription') { + if (defined($SONOS_TransportSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_TransportSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_TransportSubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'Transport-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! Transport-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + + if (defined($SONOS_RenderingSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_RenderingSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_RenderingSubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'Rendering-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! Rendering-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + + if (defined($SONOS_AlarmSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_AlarmSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_AlarmSubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'Alarm-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! Alarm-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + + if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_ZoneGroupTopologySubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_ZoneGroupTopologySubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'ZoneGroupTopology-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! ZoneGroupTopology-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + + if (defined($SONOS_DevicePropertiesSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_DevicePropertiesSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_DevicePropertiesSubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'DeviceProperties-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! DeviceProperties-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + + if (defined($SONOS_AudioInSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_AudioInSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) { + eval { + $SONOS_AudioInSubscriptions{$udn}->renew(); + SONOS_Log $udn, 3, 'AudioIn-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.'; + }; + if ($@) { + SONOS_Log $udn, 3, 'Error! AudioIn-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@; + + # Wenn der Player nicht erreichbar war, dann entsprechend entfernen... + # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll... + if ($@ =~ m/Can.t connect to/) { + SONOS_DeleteProxyObjects($udn); + } + } + } + } elsif ($workType eq 'playURI') { + my $songURI = SONOS_ExpandURIForQueueing($params[0]); + + my $volume; + if ($#params > 0) { + $volume = $params[1]; + } + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my ($uri, $meta) = SONOS_CreateURIMeta($songURI); + $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $uri, $meta); + + if (defined($volume)) { + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0); + if ($volume =~ m/^[+-]{1}/) { + $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume) + } else { + $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume); + } + } + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1)->isSuccessful)); + } + } elsif ($workType eq 'playURITemp') { + my $destURL = $params[0]; + + my $volume; + if ($#params > 0) { + $volume = $params[1]; + } + + SONOS_PlayURITemp($udn, $destURL, $volume); + } elsif ($workType eq 'addURIToQueue') { + my $songURI = SONOS_ExpandURIForQueueing($params[0]); + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $track = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('Track'); + + my ($uri, $meta) = SONOS_CreateURIMeta($songURI); + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $uri, $meta, $track + 1, 1))); + } + } elsif ($workType =~ m/speak\d+/i) { + my $volume = $params[0]; + my $language = $params[1]; + + my $text = $params[2]; + for(my $i = 3; $i < @params; $i++) { + $text .= ','.$params[$i]; + } + $text =~ s/^ *(.*) *$/$1/g; + $text = SONOS_Utf8ToLatin1($text); + + my $digest = ''; + if (SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileHashCache', 0) == 1) { + eval { + require Digest::SHA1; + import Digest::SHA1 qw(sha1_hex); + $digest = '_'.sha1_hex(lc($text)); + }; + if ($@ =~ /Can't locate Digest\/SHA1.pm in/i) { + # Unter Ubuntu gibt es die SHA1-Library nicht mehr, sodass man dort eine andere einbinden muss (SHA) + eval { + require Digest::SHA; + import Digest::SHA qw(sha1_hex); + $digest = '_'.sha1_hex(lc($text)); + }; + } + if ($@) { + SONOS_Log $udn, 2, 'Beim Ermitteln des Hash-Wertes ist ein Fehler aufgetreten: '.$@; + return; + } + } + + my $timestamp = ''; + if (!$digest && SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakFileTimestamp', 0) == 1) { + my @timearray = localtime; + $timestamp = sprintf("_%04d%02d%02d-%02d%02d%02d", $timearray[5]+1900,$timearray[4]+1,$timearray[3], $timearray[2],$timearray[1],$timearray[0]); + } + + my $fileExtension = SONOS_GetSpeakFileExtension($workType); + my $dest = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakDir', '.').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension; + my $destURL = SONOS_Client_Data_Retreive('undef', 'attr', 'targetSpeakURL', '').'/'.$udn.'_Speak'.$timestamp.$digest.'.'.$fileExtension; + + if ($digest && (-e $dest)) { + SONOS_Log $udn, 3, 'Hole die Durchsage aus dem Cache...'; + } else { + if (!SONOS_GetSpeakFile($udn, $workType, $language, $text, $dest)) { + return; + } + + # MP3-Tags setzen, wenn die entsprechende Library gefunden wurde, und die Ausgabe in ein MP3-Format erfolgte + if (lc(substr($dest, -3, 3)) eq 'mp3') { + eval { + my $mp3GroundPath = SONOS_GetAbsolutePath($0); + $mp3GroundPath = substr($mp3GroundPath, 0, rindex($mp3GroundPath, '/')); + + require MP3::Tag; + my $mp3 = MP3::Tag->new($dest); + + $mp3->title_set($text); + $mp3->artist_set('FHEM ~ Sonos'); + $mp3->album_set('Sprachdurchsagen'); + my $coverPath = SONOS_Client_Data_Retreive('undef', 'attr', ucfirst(lc(($workType =~ /0$/) ? 'speak' : $workType)).'Cover', $mp3GroundPath.'/www/images/default/fhemicon.png'); + my $imgfile = SONOS_ReadFile($coverPath); + $mp3->set_id3v2_frame('APIC', 0, (($coverPath =~ m/\.png$/) ? 'image/png' : 'image/jpeg'), chr(3), 'Cover Image', $imgfile) if ($imgfile); + $mp3->update_tags(); + }; + if ($@) { + SONOS_Log $udn, 2, 'Beim Setzen der MP3-Informationen (ID3TagV2) ist ein Fehler aufgetreten: '.$@; + } + } + } + + SONOS_PlayURITemp($udn, $destURL, $volume); + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DoWork-Syntax ERROR'); + } + }; + if ($@) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'DoWork-Exception ERROR: '.$@); + } + + $SONOS_ComObjectTransportQueue->dequeue(); + } + + return 1; + }; + + my $error; + do { + eval { + $SONOS_Controlpoint = UPnP::ControlPoint->new(SearchPort => 8008 + threads->tid() - 1, SubscriptionPort => 9009 + threads->tid() - 1, SubscriptionURL => '/eventSub', MaxWait => 30, IgnoreIP => \%IgnoreIPs); + $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback); + $SONOS_Controlpoint->handle; + }; + $error = $@; + + # Nur wenn es der Fehler mit der XML-Struktur ist, dann den UPnP-Handler nochmal anstarten... + if (($error =~ m/multiple roots, wrong element '.*?'/si) || ($error =~ m/junk '.*?' after XML element/si) || ($error =~ m/mismatched tag '.*?'/si) || ($error =~ m/500 Can't connect to/si)) { + SONOS_Log undef, 2, "Error during UPnP-Handling, restarting handling: $error"; + SONOS_StopControlPoint(); + } else { + SONOS_Log undef, 2, "Error during UPnP-Handling: $error"; + SONOS_StopControlPoint(); + + # => Vielleicht noch auskommentieren + undef($error); + } + } while ($error); + + SONOS_Log undef, 3, 'UPnP-Thread wurde beendet.'; + $SONOS_Thread = -1; + + return 1; +} + +######################################################################################## +# +# SONOS_MakeCoverURL - Generates the approbriate cover-url incl. the use of an Fhem-Proxy +# +######################################################################################## +sub SONOS_MakeCoverURL($$) { + my ($udn, $resURL) = @_; + + if ($resURL =~ m/^(x-rincon-cpcontainer|x-sonos-spotify).*?(spotify.*?)(\?|$)/i) { + my $infos = get('https://embed.spotify.com/oembed/?url='.$2); + + if ($infos =~ m/"thumbnail_url":"(.*?)cover(.*?)"/i) { + $resURL = $1.'original'.$2; + $resURL =~ s/\\//g; + } + } elsif($resURL =~ m/savedqueues/i) { + $resURL = '/fhem/sonos/cover/playlist.jpg'; + } else { + my $stream = 0; + $stream = 1 if ($resURL =~ /x-sonosapi-stream/); + $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.uri_escape($resURL) if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i); + } + + # Alles über Fhem als Proxy laufen lassen? + $resURL = '/fhem/sonos/proxy/aa?url='.uri_escape($resURL) if (($resURL !~ m/^\//) && SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0)); + + return $resURL; +} + +######################################################################################## +# +# SONOS_GetSpeakFileExtension - Retrieves the desired fileextension +# +######################################################################################## +sub SONOS_GetSpeakFileExtension($) { + my ($workType) = @_; + + if (lc($workType) eq 'speak0') { + return 'mp3'; + } elsif ($workType =~ m/speak\d+/i) { + $workType = ucfirst(lc($workType)); + + my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0); + if ($speakDefinition =~ m/(.*?):(.*)/) { + return $1; + } + } + + return ''; +} + +######################################################################################## +# +# SONOS_GetSpeakFile - Generates the audiofile according to the given text, language and generator +# +######################################################################################## +sub SONOS_GetSpeakFile($$$$$) { + my ($udn, $workType, $language, $text, $destFileName) = @_; + + if (lc($workType) eq 'speak0') { + # Chunks ermitteln... + # my @textList = ($text =~ m/(?:\b(?:[^ ]+)\W*){0,$SONOS_GOOGLETRANSLATOR_CHUNKSIZE}/g); + # pop @textList; # Letztes Element ist immer leer, deshalb abschneiden... + my @textList = (''); + for my $elem (split(/[ \t]/, $text)) { + if (length($textList[$#textList].' '.$elem) <= $SONOS_GOOGLETRANSLATOR_CHUNKSIZE) { + $textList[$#textList] .= ' '.$elem; + } else { + push(@textList, $elem); + } + } + SONOS_Log $udn, 5, 'Chunks: '.SONOS_Stringify(\@textList); + + # Einzelne Chunks herunterladen... + my $counter = 0; + for my $text (@textList) { + $counter++; + + my $url = 'http://translate.google.com/translate_tts?tl='.uri_escape(lc($language)).'&q='.uri_escape($text); + + SONOS_Log $udn, 3, 'Load Google generated MP3 ('.$counter.'. Element) from "'.$url.'" to "'.$destFileName.$counter.'"'; + + my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11'); + my $response = $ua->get($url, ':content_file' => $destFileName.$counter); + if (!$response->is_success) { + SONOS_Log $udn, 1, 'MP3 Download-Error: '.$response->status_line; + unlink($destFileName.$counter); + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during downloading.'); + return 0; + } + } + + # Heruntergeladene Chunks zusammenführen... + SONOS_Log $udn, 3, 'Combine loaded chunks into "'.$destFileName.'"'; + + # Reinladen + my $newMP3File = ''; + for(my $i = 1; $i <= $counter; $i++) { + $newMP3File .= SONOS_ReadFile($destFileName.$i); + unlink($destFileName.$i) + } + # Speichern + if (defined(open MPFILE, '>'.$destFileName)) { + binmode MPFILE ; + print MPFILE $newMP3File; + close MPFILE; + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': MP3-Creation ERROR during combining.'); + return 0; + } + + return 1; + } elsif ($workType =~ m/speak\d+/i) { + $workType = ucfirst(lc($workType)); + SONOS_Log $udn, 3, 'Load '.$workType.' generated SpeakFile to "'.$destFileName.'"'; + + my $speakDefinition = SONOS_Client_Data_Retreive('undef', 'attr', $workType, 0); + if ($speakDefinition =~ m/(.*?):(.*)/) { + $speakDefinition = $2; + + $speakDefinition =~ s/%language%/$language/gi; + $speakDefinition =~ s/%filename%/$destFileName/gi; + $speakDefinition =~ s/%text%/$text/gi; + + SONOS_Log $udn, 5, 'Execute: '.$speakDefinition; + system($speakDefinition); + + return 1; + } else { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': No Definition found!'); + return 0; + } + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Speaking not defined.'); + return 0; +} + +######################################################################################## +# +# SONOS_CreateURIMeta - Creates the Meta-Information according to the Song-URI +# +# Parameter $res = The URI to the song, for which the Metadata has to be generated +# +######################################################################################## +sub SONOS_CreateURIMeta($) { + my ($res) = @_; + my $meta = $SONOS_DIDLHeader.'object.item.audioItem.musicTrackRINCON_AssociatedZPUDN'.$SONOS_DIDLFooter; + + my $userID_Spotify = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Spotify', '-')); + my $userID_Napster = uri_unescape(SONOS_Client_Data_Retreive('undef', 'reading', 'UserID_Napster', '-')); + + # Wenn es ein Spotify- oder Napster-Titel ist, dann den Benutzernamen extrahieren + if ($res =~ m/^(x-sonos-spotify:)(.*?)(\?.*?)/) { + if ($userID_Spotify eq '-') { + SONOS_Log undef, 1, 'There are Spotify-Titles in list, and no Spotify-Username is known. Please empty the main queue and insert a random spotify-title in it for saving this information and do this action again!'; + return; + } + + $res = $1.uri_escape($2).$3; + $meta = $SONOS_DIDLHeader.'object.item.audioItem.musicTrack'.$userID_Spotify.''.$SONOS_DIDLFooter; + } elsif ($res =~ m/^(npsdy:)(.*?)(\.mp3)/) { + if ($userID_Napster eq '-') { + SONOS_Log undef, 1, 'There are Napster/Rhapsody-Titles in list, and no Napster-Username is known. Please empty the main queue and insert a random napster-title in it for saving this information and do this action again!'; + return; + } + + $res = $1.uri_escape($2).$3; + $meta = $SONOS_DIDLHeader.'object.item.audioItem.musicTrack'.$userID_Napster.''.$SONOS_DIDLFooter; + } else { + $res =~ s/ /%20/ig; + $res =~ s/"/"/ig; + } + + return ($res, $meta); +} + +######################################################################################## +# +# SONOS_CheckAlarmHash - Checks if the given hash has all neccessary Alarm-Parameters +# Additionally it converts some parameters for direct use for Zoneplayer-Update +# +# Parameter %old = All neccessary informations to check +# +######################################################################################## +sub SONOS_CheckAndCorrectAlarmHash($) { + my ($hash) = @_; + + # Checks, if a value is missing + my @keys = keys(%$hash); + if ((!SONOS_isInList('StartTime', @keys)) + || (!SONOS_isInList('Duration', @keys)) + || (!SONOS_isInList('Recurrence_Once', @keys)) + || (!SONOS_isInList('Recurrence_Monday', @keys)) + || (!SONOS_isInList('Recurrence_Tuesday', @keys)) + || (!SONOS_isInList('Recurrence_Wednesday', @keys)) + || (!SONOS_isInList('Recurrence_Thursday', @keys)) + || (!SONOS_isInList('Recurrence_Friday', @keys)) + || (!SONOS_isInList('Recurrence_Saturday', @keys)) + || (!SONOS_isInList('Recurrence_Sunday', @keys)) + || (!SONOS_isInList('Enabled', @keys)) + || (!SONOS_isInList('RoomUUID', @keys)) + || (!SONOS_isInList('ProgramURI', @keys)) + || (!SONOS_isInList('ProgramMetaData', @keys)) + || (!SONOS_isInList('Shuffle', @keys)) + || (!SONOS_isInList('Repeat', @keys)) + || (!SONOS_isInList('Volume', @keys)) + || (!SONOS_isInList('IncludeLinkedZones', @keys))) { + return 0; + } + + # Converts some values + # Playmode + $hash->{PlayMode} = 'NORMAL'; + $hash->{PlayMode} = 'SHUFFLE' if ($hash->{Repeat} && $hash->{Shuffle}); + $hash->{PlayMode} = 'SHUFFLE_NOREPEAT' if (!$hash->{Repeat} && $hash->{Shuffle}); + $hash->{PlayMode} = 'REPEAT_ALL' if ($hash->{Repeat} && !$hash->{Shuffle}); + + # Recurrence + if ($hash->{Recurrence_Once}) { + $hash->{Recurrence} = 'ONCE'; + } else { + $hash->{Recurrence} = 'ON_'; + $hash->{Recurrence} .= '1' if ($hash->{Recurrence_Monday}); + $hash->{Recurrence} .= '2' if ($hash->{Recurrence_Tuesday}); + $hash->{Recurrence} .= '3' if ($hash->{Recurrence_Wednesday}); + $hash->{Recurrence} .= '4' if ($hash->{Recurrence_Thursday}); + $hash->{Recurrence} .= '5' if ($hash->{Recurrence_Friday}); + $hash->{Recurrence} .= '6' if ($hash->{Recurrence_Saturday}); + $hash->{Recurrence} .= '7' if ($hash->{Recurrence_Sunday}); + } + + # If nothing is given, set 'ONCE' + if ($hash->{Recurrence} eq 'ON_') { + $hash->{Recurrence} = 'ONCE'; + } + + return 1; +} + +######################################################################################## +# +# SONOS_RestoreOldPlaystate - Restores the old Position of a playing state +# +######################################################################################## +sub SONOS_RestoreOldPlaystate() { + SONOS_Log undef, 1, 'Restore-Thread gestartet. Warte auf Arbeit...'; + + my $runEndlessLoop = 1; + my $controlPoint = UPnP::ControlPoint->new(SearchPort => 8008 + threads->tid() - 1, SubscriptionPort => 9009 + threads->tid() - 1, SubscriptionURL => '/eventSub', MaxWait => 20, IgnoreIP => \%IgnoreIPs); + + $SIG{'PIPE'} = 'IGNORE'; + $SIG{'CHLD'} = 'IGNORE'; + + $SIG{'INT'} = sub { + $runEndlessLoop = 0; + }; + + while ($runEndlessLoop) { + select(undef, undef, undef, 0.2); + next if (!$SONOS_PlayerRestoreQueue->pending()); + + # Es ist was auf der Queue... versuchen zu verarbeiten... + my %old = %{$SONOS_PlayerRestoreQueue->peek()}; + + # Wenn die Zeit noch nicht reif ist, dann doch wieder übergehen... + # Dabei die Schleife wieder von vorne beginnen lassen, da noch andere dazwischengeschoben werden könnten. + # Eine Weile in die Zukunft, da das ermitteln der Proxies Zeit benötigt. + next if ($old{RestoreTime} > time() + 1); + + # ...sonst das Ding von der Queue nehmen... + $SONOS_PlayerRestoreQueue->dequeue(); + + # Hier die ursprünglichen Proxies wiederherstellen/neu verbinden... + my $device = $controlPoint->_createDevice($old{location}); + my $AVProxy; + my $GRProxy; + my $CCProxy; + for my $subdevice ($device->children) { + if ($subdevice->UDN =~ /.*_MR/i) { + $AVProxy = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1')->controlProxy(); + $GRProxy = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')->controlProxy(); + } + + if ($subdevice->UDN =~ /.*_MS/i) { + $CCProxy = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')->controlProxy(); + } + } + my $udn = $device->UDN.'_MR'; + $udn =~ s/.*?:(.*)/$1/; + + SONOS_Log $udn.'_MR', 3, 'Restorethread has found a job. Waiting for stop playing...'; + + # Ist das Ding fertig abgespielt? + my $result; + do { + select(undef, undef, undef, 0.5); + $result = $AVProxy->GetTransportInfo(0); + } while ($result->getValue('CurrentTransportState') ne 'STOPPED'); + + + SONOS_Log $udn, 3, 'Restoring playerstate...'; + # Die Liste als aktuelles Abspielstück einstellen, oder den Stream wieder anwerfen + if ($old{CurrentURI} =~ /^x-.*?-.*?stream/) { + $AVProxy->SetAVTransportURI(0, $old{CurrentURI}, $old{CurrentURIMetaData}); + } else { + my $queueMetadata = $CCProxy->Browse('Q:0', 'BrowseMetadata', '', 0, 0, ''); + $AVProxy->SetAVTransportURI(0, SONOS_GetTagData('res', $queueMetadata->getValue('Result')), ''); + + $AVProxy->Seek(0, 'TRACK_NR', $old{Track}); + $AVProxy->Seek(0, 'REL_TIME', $old{RelTime}); + } + + my $oldMute = $GRProxy->GetGroupMute(0)->getValue('CurrentMute'); + $GRProxy->SetGroupMute(0, $old{Mute}) if (defined($old{Mute}) && ($old{Mute} != $oldMute)); + + my $oldVolume = $GRProxy->GetGroupVolume(0)->getValue('CurrentVolume'); + $GRProxy->SetGroupVolume(0, $old{Volume}) if (defined($old{Volume}) && ($old{Volume} != $oldVolume)); + + if (($old{CurrentTransportState} eq 'PLAYING') || ($old{CurrentTransportState} eq 'TRANSITIONING')) { + $AVProxy->Play(0, 1); + } elsif ($old{CurrentTransportState} eq 'PAUSED_PLAYBACK') { + $AVProxy->Pause(0); + } + + $SONOS_PlayerRestoreRunningUDN{$udn} = 0; + SONOS_Log $udn, 3, 'Playerstate restored!'; + } + + undef($controlPoint); + + SONOS_Log undef, 1, 'Restore-Thread wurde beendet.'; + $SONOS_Thread_PlayerRestore = -1; +} + +######################################################################################## +# +# SONOS_PlayURITemp - Plays an URI temporary +# +# Parameter $udn = The udn of the SonosPlayer +# $destURLParam = URI, that has to be played +# $volumeParam = Volume for playing +# +######################################################################################## +sub SONOS_PlayURITemp($$$) { + my ($udn, $destURLParam, $volumeParam) = @_; + + my %old; + $old{DestURIOriginal} = $destURLParam; + my ($songURI, $meta) = SONOS_CreateURIMeta(SONOS_ExpandURIForQueueing($old{DestURIOriginal})); + + # Wenn auf diesem Player bereits eine temporäre Wiedergabe erfolgt, dann hier auf dessen Beendigung warten... + if (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) { + SONOS_Log $udn, 3, 'Temporary playing of "'.$old{DestURIOriginal}.'" must wait, because another playing is in work...'; + + while (defined($SONOS_PlayerRestoreRunningUDN{$udn}) && $SONOS_PlayerRestoreRunningUDN{$udn}) { + select(undef, undef, undef, 0.2); + } + } + + $SONOS_PlayerRestoreRunningUDN{$udn} = 1; + + SONOS_Log $udn, 3, 'Start temporary playing of "'.$old{DestURIOriginal}.'"'; + + my $volume = $volumeParam; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + $old{UDN} = $udn; + + my $result = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0); + $old{Track} = $result->getValue('Track'); + $old{RelTime} = $result->getValue('RelTime'); + + $result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0); + $old{CurrentURI} = $result->getValue('CurrentURI'); + $old{CurrentURIMetaData} = $result->getValue('CurrentURIMetaData'); + + $result = $SONOS_AVTransportControlProxy{$udn}->GetTransportInfo(0); + $old{CurrentTransportState} = $result->getValue('CurrentTransportState'); + + $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $songURI, $meta); + + if (SONOS_CheckProxyObject($udn, $SONOS_GroupRenderingControlProxy{$udn})) { + $SONOS_GroupRenderingControlProxy{$udn}->SnapshotGroupVolume(0); + + $old{Mute} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupMute(0)->getValue('CurrentMute'); + $SONOS_GroupRenderingControlProxy{$udn}->SetGroupMute(0, 0) if $old{Mute}; + + $old{Volume} = $SONOS_GroupRenderingControlProxy{$udn}->GetGroupVolume(0)->getValue('CurrentVolume'); + if (defined($volume)) { + if ($volume =~ m/^[+-]{1}/) { + $SONOS_GroupRenderingControlProxy{$udn}->SetRelativeGroupVolume(0, $volume) if $volume; + } else { + $SONOS_GroupRenderingControlProxy{$udn}->SetGroupVolume(0, $volume) if ($volume != $old{Volume}); + } + } + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'PlayURITemp: '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->Play(0, 1))); + + SONOS_Log $udn, 4, 'All is started successfully. Retreive Positioninfo...'; + $old{SleepTime} = 0; + eval { + $old{SleepTime} = SONOS_GetTimeSeconds($SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('TrackDuration')); + + # Wenn es keine Laufzeitangabe gibt, dann muss diese selber berechnet werden, sofern möglich. Sollte dies nicht möglich sein, ist dies vermutlich ein Stream... + if ($old{SleepTime} == 0) { + SONOS_Log $udn, 3, 'SleepTimer berechnet die Laufzeit des Titels selber, da keine Wartezeit uebermittelt wurde!'; + + eval { + use MP3::Info; + my $tag = get_mp3info($old{DestURIOriginal}); + if ($tag) { + $old{SleepTime} = $tag->{SECS}; + } + }; + if ($@) { + SONOS_Log $udn, 2, 'Bei der MP3-Längenermittlung ist ein Fehler aufgetreten: '.$@; + } + } + + $old{RestoreTime} = time() + $old{SleepTime} - 1; + SONOS_Log $udn, 3, 'Laufzeitermittlung abgeschlossen: '.$old{SleepTime}.'s, Restore-Zeit: '.GetTimeString($old{RestoreTime}); + }; + + # Location mitsichern, damit die Proxies neu geholt werden können + my %revUDNs = reverse %SONOS_Locations; + $old{location} = $revUDNs{$udn}; + + # Restore-Daten an der richtigen Stelle auf die Queue legen, damit der Player-Restore-Thread sich darum kümmern kann + # Aber nur, wenn auch ein Restore erfolgen kann, weil eine Zeit existiert + if (defined($old{SleepTime}) && ($old{SleepTime} != 0)) { + my $i; + for ($i = $SONOS_PlayerRestoreQueue->pending() - 1; $i >= 0; $i--) { + my %tmpOld = %{$SONOS_PlayerRestoreQueue->peek($i)}; + last if ($old{RestoreTime} > $tmpOld{RestoreTime}); + } + + $SONOS_PlayerRestoreQueue->insert($i + 1, \%old); + } else { + SONOS_Log $udn, 1, 'Da keine Endzeit ermittelt werden konnte, wird kein Restoring durchgeführt werden!'; + $SONOS_PlayerRestoreRunningUDN{$udn} = 0; + } + } +} + +######################################################################################## +# +# SONOS_ExpandURIForQueueing - Expands and corrects a given URI +# +# Parameter $songURI = The URI that has to be converted +# +######################################################################################## +sub SONOS_ExpandURIForQueueing($) { + my ($songURI) = @_; + + # Backslashe umwandeln + $songURI =~ s/\\/\//g; + + # SongURI erweitern/korrigieren + $songURI = 'x-file-cifs:'.$songURI if ($songURI =~ m/^\/\//); + $songURI = 'x-rincon-mp3radio:'.$1 if ($songURI =~ m/^http:(\/\/.*)/); + + return $songURI; +} + +######################################################################################## +# +# SONOS_GetURIFromQueueValue - Gets the URI from current Informations +# +# Parameter $songURI = The URI that has to be converted +# +######################################################################################## +sub SONOS_GetURIFromQueueValue($) { + my ($songURI) = @_; + + # SongURI erweitern/korrigieren + $songURI = $1 if ($songURI =~ m/^x-file-cifs:(.*)/i); + $songURI = 'http:'.$1 if ($songURI =~ m/^x-rincon-mp3radio:(.*)/i); + $songURI = uri_unescape($songURI) if ($songURI =~ m/^x-sonos-spotify:/i); + + return $songURI; +} + +######################################################################################## +# +# SONOS_GetTimeSeconds - Converts a Time-String like '0:04:12' to seconds (e.g. 252) +# +# Parameter $timeStr = The timeStr that has to be converted +# +######################################################################################## +sub SONOS_GetTimeSeconds($) { + my ($timeStr) = @_; + + return SONOS_Max(int($1)*3600 + int($2)*60 + int($3), 1) if ($timeStr =~ m/(\d+):(\d+):(\d+)/); + return 0; +} + +######################################################################################## +# +# SONOS_CheckProxyObject - Checks for existence of $proxyObject (=return 1) or not (=return 0). Additionally in case of error it lays an error-answer in the queue +# +# Parameter $proxyObject = The Proxy that has to be checked +# +######################################################################################## +sub SONOS_CheckProxyObject($$) { + my ($udn, $proxyObject) = @_; + + if (defined($proxyObject)) { + SONOS_Log $udn, 4, 'ProxyObject exists: '.$proxyObject; + + return 1; + } else { + SONOS_Log $udn, 3, 'ProxyObject does not exists'; + + # Das Aufräumen der ProxyObjects und das Erzeugen des Notify wurde absichtlich nicht hier reingeschrieben, da es besser im IsAlive-Checker aufgehoben ist. + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', 'CheckProxyObject-ERROR: SonosPlayer disappeared?'); + return 0; + } +} + +######################################################################################## +# +# SONOS_MakeSigHandlerReturnValue - Enqueue all necessary elements on upward-queue +# +# Parameter $returnValue = The value that has to be laid on the queue. +# +######################################################################################## +sub SONOS_MakeSigHandlerReturnValue($$$) { + my ($udn, $returnName, $returnValue) = @_; + + #Antwort melden + SONOS_Client_Notifier('DoWorkAnswer:'.$udn.':'.$returnName.':'.$returnValue); +} + +######################################################################################## +# +# SONOS_StopControlPoint - Stops all open Net-Handles and Search-Token of the UPnP Part +# +######################################################################################## +sub SONOS_StopControlPoint { + if (defined($SONOS_Controlpoint)) { + $SONOS_Controlpoint->stopSearch($SONOS_Search); + $SONOS_Controlpoint->stopHandling(); + undef($SONOS_Controlpoint); + + SONOS_Log undef, 4, 'ControlPoint is successfully stopped!'; + } +} + +######################################################################################## +# +# SONOS_GetTagData - Return the content of the given tag in the given string +# +# Parameter $tagName = The tag to be searched for +# $data = The string in which to search for +# +######################################################################################## +sub SONOS_GetTagData($$) { + my ($tagName, $data) = @_; + + return $1 if ($data =~ m/<$tagName.*?>(.*?)<\/$tagName>/i); + return ''; +} + +######################################################################################## +# +# SONOS_AnswerMessage - Return 'Success' if param is true, 'Error' otherwise +# +# Parameter $var = The value to check +# +######################################################################################## +sub SONOS_AnswerMessage($) { + my ($var) = @_; + + if ($var) { + return 'Success!'; + } else { + return 'Error!'; + } +} + +######################################################################################## +# +# SONOS_UPnPAnswerMessage - Return 'Success' if param is true, a complete error-message of the UPnP-answer otherwise +# +# Parameter $var = The UPnP-answer to check +# +######################################################################################## +sub SONOS_UPnPAnswerMessage($) { + my ($var) = @_; + + if ($var->isSuccessful) { + return 'Success!'; + } else { + my $faultcode = '-'; + my $faultstring = '-'; + my $faultactor = '-'; + my $faultdetail = '-'; + + $faultcode = $var->faultcode if ($var->faultcode); + $faultstring = $var->faultstring if ($var->faultstring); + $faultactor = $var->faultactor if ($var->faultactor); + $faultdetail = $var->faultdetail if ($var->faultdetail); + + return 'Error! UPnP-Fault-Fields: Code: "'.$faultcode.'", String: "'.$faultstring.'", Actor: "'.$faultactor.'", Detail: "'.SONOS_Stringify($faultdetail).'"'; + } +} + +######################################################################################## +# +# SONOS_Stringify - Converts a given Value (Array, Hash, Scalar) to a readable string version +# +# Parameter $varRef = The value to convert to a readable version +# +######################################################################################## +sub SONOS_Stringify { + my ($varRef) = @_; + + return 'undef' if (!defined($varRef)); + + my $reftype = reftype $varRef; + if (!defined($reftype) || ($reftype eq '')) { + if (looks_like_number($varRef)) { + return $varRef; + } else { + $varRef =~ s/'/\\'/g; + return "'".$varRef."'"; + } + } elsif ($reftype eq 'HASH') { + my %var = %{$varRef}; + + my @result; + foreach my $key (keys %var) { + push(@result, $key.' => '.SONOS_Stringify($var{$key})); + } + + return '{'.join(', ', @result).'}'; + } elsif ($reftype eq 'ARRAY') { + my @var = @{$varRef}; + + my @result; + foreach my $value (@var) { + push(@result, SONOS_Stringify($value)); + } + + return '['.join(', ', @result).']'; + } elsif ($reftype eq 'SCALAR') { + if (looks_like_number(${$varRef})) { + return ${$varRef}; + } else { + ${$varRef} =~ s/'/\\'/g; + return "'".${$varRef}."'"; + } + } else { + return 'Unsupported Type ('.$reftype.') of: '.$varRef; + } +} + +######################################################################################## +# +# SONOS_UmlautConvert - Converts any umlaut (e.g. ä) to Ascii-conform writing (e.g. ae) +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_UmlautConvert($) { + my ($var) = @_; + + if ($var eq 'ä') { + return 'ae'; + } elsif ($var eq 'ö') { + return 'oe'; + } elsif ($var eq 'ü') { + return 'ue'; + } elsif ($var eq 'Ä') { + return 'Ae'; + } elsif ($var eq 'Ö') { + return 'Oe'; + } elsif ($var eq 'Ü') { + return 'Ue'; + } elsif ($var eq 'ß') { + return 'ss'; + } else { + return '_'; + } +} + +######################################################################################## +# +# SONOS_ConvertUmlautToHtml - Converts any umlaut (e.g. ä) to Html-conform writing (e.g. ä) +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_ConvertUmlautToHtml($) { + my ($var) = @_; + + if ($var eq 'ä') { + return 'ä'; + } elsif ($var eq 'ö') { + return 'ö'; + } elsif ($var eq 'ü') { + return 'ü'; + } elsif ($var eq 'Ä') { + return 'Ä'; + } elsif ($var eq 'Ö') { + return 'Ö'; + } elsif ($var eq 'Ü') { + return 'Ü'; + } elsif ($var eq 'ß') { + return 'ß'; + } else { + return $var; + } +} + +######################################################################################## +# +# SONOS_Latin1ToUtf8 - Converts Latin1 coding to UTF8 +# +# Parameter $var = The value to convert +# +# http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS +# +######################################################################################## +sub SONOS_Latin1ToUtf8($) { + my ($s)= @_; + + $s =~ s/([\x80-\xFF])/chr(0xC0|ord($1)>>6).chr(0x80|ord($1)&0x3F)/eg; + + return $s; +} + +######################################################################################## +# +# SONOS_Utf8ToLatin1 - Converts UTF8 coding to Latin1 +# +# Parameter $var = The value to convert +# +# http://perldoc.perl.org/perluniintro.html, UNICODE IN OLDER PERLS +# +######################################################################################## +sub SONOS_Utf8ToLatin1($) { + my ($s)= @_; + + $s =~ s/([\xC2\xC3])([\x80-\xBF])/chr(ord($1)<<6&0xC0|ord($2)&0x3F)/eg; + + return $s; +} + +######################################################################################## +# +# SONOS_ConvertNumToWord - Converts the values "0, 1" to "off, on" +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_ConvertNumToWord($) { + my ($var) = @_; + + if (!looks_like_number($var)) { + return 'on' if (lc($var) ne 'off'); + return 'off'; + } + + if ($var == 0) { + return 'off'; + } else { + return 'on'; + } +} + +######################################################################################## +# +# SONOS_ConvertWordToNum - Converts the values "off, on" to "0, 1" +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_ConvertWordToNum($) { + my ($var) = @_; + + if (looks_like_number($var)) { + return 1 if ($var != 0); + return 0; + } + + if (lc($var) eq 'off') { + return 0; + } else { + return 1; + } +} + +######################################################################################## +# +# SONOS_ToggleNum - Convert the values "0, 1" to "1, 0" +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_ToggleNum($) { + my ($var) = @_; + + if ($var == 0) { + return 1; + } else { + return 0; + } +} + +######################################################################################## +# +# SONOS_ToggleWord - Convert the values "off, on" to "on, off" +# +# Parameter $var = The value to convert +# +######################################################################################## +sub SONOS_ToggleWord($) { + my ($var) = @_; + + if (lc($var) eq 'off') { + return 'on'; + } else { + return 'off'; + } +} + +######################################################################################## +# +# SONOS_Discover_Callback - Discover-Callback, +# autocreate devices if not already present +# +# Parameter $search = +# $device = +# $action = +# +######################################################################################## +sub SONOS_Discover_Callback($$$) { + my ($search, $device, $action) = @_; + + # Sicherheitsabfrage, da offensichtlich manchmal falsche Elemente durchkommen... + if ($device->deviceType() ne 'urn:schemas-upnp-org:device:ZonePlayer:1') { + SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received!'; + return; + } + + if ($action eq 'deviceAdded') { + my $descriptionDocument; + eval { + $descriptionDocument = decode(SONOS_Client_Data_Retreive('undef', 'attr', 'characterDecoding', 'CP-1252'), $device->descriptionDocument()); + }; + if ($@) { + # Das Descriptiondocument konnte nicht abgefragt werden + SONOS_Log undef, 2, 'Discover-Event: Wrong deviceType "'.$device->deviceType().'" received! Detected while trying to download the Description-Document from Player.'; + return; + } + + # Wenn kein Description-Dokument geliefert wurde... + if (!defined($descriptionDocument) || ($descriptionDocument eq '')) { + SONOS_Log undef, 2, "Discover-Event: Description-Document is empty. Aborting this deviceadding-process."; + return; + } + + # Alles OK, es kann weitergehen + SONOS_Log undef, 4, "Discover-Event: Description-Document: $descriptionDocument"; + + $SONOS_Client_SendQueue_Suspend = 1; + + # Variablen initialisieren + my $roomName = ''; + my $saveRoomName = ''; + my $modelNumber = ''; + my $displayVersion = ''; + my $serialNum = ''; + my $iconURI = ''; + + # Um einen XML-Parser zu vermeiden, werden hier reguläre Ausdrücke für die Ermittlung der Werte eingesetzt... + # RoomName ermitteln + $roomName = decode_entities($1) if ($descriptionDocument =~ m/(.*?)<\/roomName>/im); + $saveRoomName = decode('UTF-8', $roomName); + $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden... + $saveRoomName =~ s/[^a-zA-Z0-9]/_/g; + my $groupName = $saveRoomName; + + # Modelnumber ermitteln + $modelNumber = decode_entities($1) if ($descriptionDocument =~ m/(.*?)<\/modelNumber>/im); + + # DisplayVersion ermitteln + $displayVersion = decode_entities($1) if ($descriptionDocument =~ m/(.*?)<\/displayVersion>/im); + + # SerialNum ermitteln + $serialNum = decode_entities($1) if ($descriptionDocument =~ m/(.*?)<\/serialNum>/im); + + # Icon-URI ermitteln + $iconURI = decode_entities($1) if ($descriptionDocument =~ m/.*?.*?0<\/id>.*?(.*?)<\/url>.*?<\/icon>.*?<\/iconList>/sim); + + # Kompletten Pfad zum Download des ZonePlayer-Bildchens zusammenbauen + my $iconOrigPath = $device->location(); + $iconOrigPath =~ s/(http:\/\/.*?)\/.*/$1$iconURI/i; + + # Zieldateiname für das ZonePlayer-Bildchen zusammenbauen + my $iconPath = $iconURI; + $iconPath =~ s/.*\/(.*)/icoSONOSPLAYER_$1/i; + + my $udnShort = $device->UDN; + $udnShort =~ s/.*?://i; + my $udn = $udnShort.'_MR'; + + $SONOS_Locations{$device->location()} = $udn; + + my $name = $SONOS_Client_Data{SonosDeviceName}."_".$saveRoomName; + + # Erkannte Werte ausgeben... + SONOS_Log undef, 4, "RoomName: '$roomName', SaveRoomName: '$saveRoomName', ModelNumber: '$modelNumber', DisplayVersion: '$displayVersion', SerialNum: '$serialNum', IconURI: '$iconURI', IconOrigPath: '$iconOrigPath', IconPath: '$iconPath'"; + + SONOS_Log undef, 2, "Discover Sonosplayer '$roomName' ($modelNumber) Software Revision $displayVersion with ID '$udn'"; + + # ServiceProxies für spätere Aufrufe merken + my $alarmService = $device->getService('urn:schemas-upnp-org:service:AlarmClock:1'); + $SONOS_AlarmClockControlProxy{$udn} = $alarmService->controlProxy if ($alarmService); + + my $audioInService = $device->getService('urn:schemas-upnp-org:service:AudioIn:1'); + $SONOS_AudioInProxy{$udn} = $audioInService->controlProxy if ($audioInService); + + my $devicePropertiesService = $device->getService('urn:schemas-upnp-org:service:DeviceProperties:1'); + $SONOS_DevicePropertiesProxy{$udn} = $devicePropertiesService->controlProxy if ($devicePropertiesService); + #$SONOS_GroupManagementProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:GroupManagement:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:GroupManagement:1')); + #$SONOS_MusicServicesProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:MusicServices:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:MusicServices:1')); + + my $zoneGroupTopologyService = $device->getService('urn:schemas-upnp-org:service:ZoneGroupTopology:1'); + $SONOS_ZoneGroupTopologyProxy{$udn} = $zoneGroupTopologyService->controlProxy if ($zoneGroupTopologyService); + + # Bei einem Dock gibt es AVTransport nur am Hauptdevice, deshalb mal schauen, ob wir es hier bekommen können + my $transportService = $device->getService('urn:schemas-upnp-org:service:AVTransport:1'); + $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService); + + my $renderingService; + + # Hier die Subdevices durchgehen, da für die Anmeldung nur das "_MR"-Device (MediaRenderer) wichtig ist + for my $subdevice ($device->children) { + SONOS_Log undef, 4, 'SubDevice found: '.$subdevice->UDN; + + if ($subdevice->UDN =~ /.*_MR/i) { + # Wir haben hier das Media-Renderer Subdevice + $transportService = $subdevice->getService('urn:schemas-upnp-org:service:AVTransport:1'); + $SONOS_AVTransportControlProxy{$udn} = $transportService->controlProxy if ($transportService); + + $renderingService = $subdevice->getService('urn:schemas-upnp-org:service:RenderingControl:1'); + $SONOS_RenderingControlProxy{$udn} = $renderingService->controlProxy if ($renderingService); + + $SONOS_GroupRenderingControlProxy{$udn} = $subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')->controlProxy if ($subdevice->getService('urn:schemas-upnp-org:service:GroupRenderingControl:1')); + } + + if ($subdevice->UDN =~ /.*_MS/i) { + # Wir haben hier das Media-Server Subdevice + $SONOS_ContentDirectoryControlProxy{$udn} = $subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')->controlProxy if ($subdevice->getService('urn:schemas-upnp-org:service:ContentDirectory:1')); + } + } + + SONOS_Log undef, 4, 'ControlProxies wurden gesichert'; + + # ZoneTopology laden, um die Benennung der Fhem-Devices besser an die Realität anpassen zu können + my $topoType = ''; + my $fieldType = ''; + my $master = 1; + if ($SONOS_ZoneGroupTopologyProxy{$udn}) { + my $zoneGroupState = $SONOS_ZoneGroupTopologyProxy{$udn}->GetZoneGroupState()->getValue('ZoneGroupState'); + SONOS_Log undef, 5, 'ZoneGroupState: '.$zoneGroupState; + + if ($zoneGroupState =~ m/.*().*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) { + my $coordinator = $2; + my $member = $3; + + # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten? + if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) { + $topoType = '_'.$1; + } + + # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten? + if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) { + $topoType = '_'.$1; + $topoType =~ s/,/_/g; + } + + SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType; + $fieldType = substr($topoType, 1) if ($topoType); + + my $invisible = 0; + $invisible = 1 if ($member =~ m/Invisible="1"/i); + + my $isZoneBridge = 0; + $isZoneBridge = 1 if ($member =~ m/IsZoneBridge="1"/i); + + $master = !$invisible || $isZoneBridge; + } + + # Wenn der aktuelle Player der Master ist, dann kein Kürzel anhängen, + # damit gibt es immer einen Player, der den Raumnamen trägt, und die anderen enthalten Kürzel + if ($master) { + $topoType = ''; + } + } + $name .= $topoType; + $saveRoomName .= $topoType; + + # Volume laden um diese im Reading ablegen zu können + my $currentVolume = 0; + my $balance = 0; + if ($SONOS_RenderingControlProxy{$udn}) { + eval { + $currentVolume = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'Master')->getValue('CurrentVolume'); + + # Balance ermitteln + my $volumeLeft = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'LF')->getValue('CurrentVolume'); + my $volumeRight = $SONOS_RenderingControlProxy{$udn}->GetVolume(0, 'RF')->getValue('CurrentVolume'); + $balance = (-$volumeLeft) + $volumeRight; + + SONOS_Log undef, 4, 'Retrieve Current Volumelevels. Master: "'.$currentVolume.'", Balance: "'.$balance.'"'; + }; + if ($@) { + $currentVolume = 0; + $balance = 0; + SONOS_Log undef, 4, 'Couldn\'t retrieve Current Volumelevels: '. $@; + } + } else { + SONOS_Log undef, 4, 'Couldn\'t get any Volume Information due to missing RenderingControlProxy'; + } + + # Load official icon from zoneplayer and copy it to local place for FHEM-use + SONOS_Client_Notifier('getstore(\''.$iconOrigPath.'\', $attr{global}{modpath}.\'/www/images/default/'.$iconPath."');\n"); + + # Icons neu einlesen lassen + SONOS_Client_Notifier('SONOS_RefreshIconsInFHEMWEB(\'/www/images/default/'.$iconPath.'\');'); + + # Transport Informations to FHEM + # Check if this device is already defined... + if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerUDNs}})) { + push @{$SONOS_Client_Data{PlayerUDNs}}, $udn; + + # Wenn der Name schon mal verwendet wurde, dann solange ein Kürzel anhängen, bis ein freier Name gefunden wurde... + while (SONOS_isInList($name, @{$SONOS_Client_Data{PlayerNames}})) { + $name .= '_X'; + $saveRoomName .= '_X'; + + SONOS_Log undef, 2, "New Fhem-Name neccessary for '$roomName' -> '$name', ID '$udn'"; + } + push @{$SONOS_Client_Data{PlayerNames}}, $name; + + my %elemValues = (); + $SONOS_Client_Data{Buffer}->{$udn} = shared_clone(\%elemValues); + + # Define SonosPlayer-Device with attributes + SONOS_Client_Notifier('CommandDefine:'.$name.' SONOSPLAYER '.$udn); + SONOS_Client_Notifier('CommandAttr:'.$name.' room '.$SONOS_Client_Data{SonosDeviceName}); + SONOS_Client_Notifier('CommandAttr:'.$name.' group '.$groupName); + SONOS_Client_Notifier('CommandAttr:'.$name.' icon '.$iconPath); + SONOS_Client_Notifier('CommandAttr:'.$name.' sortby 1'); + SONOS_Client_Notifier('CommandAttr:'.$name.' userReadings Favourites:LastActionResult.*?GetFavouritesWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, Radios:LastActionResult.*?GetRadiosWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, Playlists:LastActionResult.*?GetPlaylistsWithCovers.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }, currentTrackPosition:LastActionResult.*?GetCurrentTrackPosition.* { if (ReadingsVal("'.$name.'", "LastActionResult", "") =~ m/.*?: (.*)/) { return $1; } }'); + + # Das folgende nicht für Bridges machen + if ($modelNumber ne 'ZB100') { + SONOS_Client_Notifier('CommandAttr:'.$name.' generateInfoSummarize1 <Album prefix=" vom Album \'" suffix="\'"/></NormalAudio> <StreamAudio><Sender suffix=":"/><SenderCurrent prefix=" \'" suffix="\' -"/><SenderInfo prefix=" "/></StreamAudio>'); + SONOS_Client_Notifier('CommandAttr:'.$name.' generateInfoSummarize2 <TransportState/><InfoSummarize1 prefix=" => "/>'); + SONOS_Client_Notifier('CommandAttr:'.$name.' generateInfoSummarize3 <Volume prefix="Lautstärke: "/><Mute instead=" ~ Kein Ton" ifempty=" ~ Ton An" emptyval="0"/> ~ Balance: <Balance ifempty="Mitte" emptyval="0"/><HeadphoneConnected instead=" ~ Kopfhörer aktiv" ifempty=" ~ Kein Kopfhörer" emptyval="0"/>'); + SONOS_Client_Notifier('CommandAttr:'.$name.' stateVariable Presence'); + SONOS_Client_Notifier('CommandAttr:'.$name.' getAlarms 1'); SONOS_Client_Data_Refresh('', $udn, 'getAlarms', 1); + SONOS_Client_Notifier('CommandAttr:'.$name.' minVolume 0'); SONOS_Client_Data_Refresh('', $udn, 'minVolume', 0); + + #SONOS_Client_Notifier('CommandAttr:'.$name.' webCmd Play:Pause:Previous:Next:VolumeD:VolumeU:MuteT'); + + # Define Weblink for AlbumArt with attributes + #if ($master) { + # SONOS_Client_Notifier('CommandDefine:AlbumArt_'.$saveRoomName.' weblink image /fhem/icons/SONOSPLAYER/'.$name.'_AlbumArt'."\n"); + # SONOS_Client_Notifier('CommandAttr:AlbumArt_'.$saveRoomName.' room '.$SONOS_Client_Data{SonosDeviceName}); + # SONOS_Client_Notifier('CommandAttr:AlbumArt_'.$saveRoomName.' htmlattr width=\'200\''); + # SONOS_Client_Notifier('CommandAttr:AlbumArt_'.$saveRoomName.' group '.$groupName); + #} + + # Define ReadingsGroup + if ($master) { + SONOS_Client_Notifier('CommandDefine:'.$name.'RG ReadingsGroup '.$name.':<{SONOS_getCoverTitleRG($DEVICE)}@infoSummarize2>'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RG room '.$SONOS_Client_Data{SonosDeviceName}); + SONOS_Client_Notifier('CommandAttr:'.$name.'RG group '.$groupName); + SONOS_Client_Notifier('CommandAttr:'.$name.'RG sortby 2'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RG noheading 1'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RG nonames 1'); + } + + # Define Readingsgroup Listen + if ($master) { + SONOS_Client_Notifier('CommandDefine:'.$name.'RG_Favourites ReadingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Favourites",1)}@Favourites>'); + SONOS_Client_Notifier('CommandDefine:'.$name.'RG_Radios ReadingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Radios",1)}@Radios>'); + SONOS_Client_Notifier('CommandDefine:'.$name.'RG_Playlists ReadingsGroup '.$name.':<{SONOS_getListRG($DEVICE,"Playlists")}@Playlists>'); + } + + # Define RemoteControl + if ($master) { + SONOS_Client_Notifier('CommandDefine:'.$name.'RC remotecontrol'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC room hidden'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC group '.$SONOS_Client_Data{SonosDeviceName}); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC rc_iconpath icons/remotecontrol'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC rc_iconprefix black_btn_'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC row00 Play:PLAY,Pause:PAUSE,Previous:REWIND,Next:FF,VolumeD:VOLDOWN,VolumeU:VOLUP,MuteT:MUTE'); + + SONOS_Client_Notifier('CommandDefine:'.$name.'RC_Notify notify '.$name.'RC set '.$name.' $EVENT'); + + SONOS_Client_Notifier('CommandDefine:'.$name.'RC_Weblink weblink htmlCode {fhem("get '.$name.'RC htmlcode", 1)}'); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC_Weblink room '.$SONOS_Client_Data{SonosDeviceName}); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC_Weblink group '.$groupName); + SONOS_Client_Notifier('CommandAttr:'.$name.'RC_Weblink sortby 3'); + } + } + + SONOS_Log undef, 1, "Successfully autocreated SonosPlayer '$saveRoomName' ($modelNumber) Software Revision $displayVersion with ID '$udn'"; + } else { + SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' ($modelNumber) with ID '$udn' is already defined and will only be updated"; + } + + # Wenn der Player noch nicht auf der "Aktiv"-Liste steht, dann draufpacken... + push @{$SONOS_Client_Data{PlayerAlive}}, $udn if (!SONOS_isInList($udn, @{$SONOS_Client_Data{PlayerAlive}})); + SONOS_Client_Data_Refresh('', $udn, 'NAME', $name); + + # Readings aktualisieren + SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'presence', 'appeared'); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Balance', $balance); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'roomName', $roomName); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'playerType', $modelNumber); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'Volume', $currentVolume); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'location', $device->location); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'softwareRevision', $displayVersion); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'serialNum', $serialNum); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'fieldType', $fieldType); + SONOS_Client_Data_Refresh('', $udn, 'LastSubscriptionsRenew', SONOS_TimeNow()); + SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn); + + SONOS_Client_Notifier('CommandAttr:'.$name.' model Sonos_'.$modelNumber); + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log undef, 2, "SonosPlayer '$saveRoomName' is now updated"; + + # AVTransport-Subscription + if ($transportService) { + $SONOS_TransportSubscriptions{$udn} = $transportService->subscribe(\&SONOS_ServiceCallback); + if (defined($SONOS_TransportSubscriptions{$udn})) { + SONOS_Log undef, 2, 'Service-subscribing successful with SID="'.$SONOS_TransportSubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_TransportSubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'Service-subscribing NOT successful'; + } + } else { + undef($SONOS_TransportSubscriptions{$udn}); + SONOS_Log undef, 1, 'Service-subscribing not possible due to missing TransportService'; + } + + # Rendering-Subscription, wenn eine untere oder obere Lautstärkegrenze angegeben wurde, und Lautstärke überhaupt geht + if ($renderingService && (SONOS_Client_Data_Retreive($udn, 'attr', 'minVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'minVolumeHeadphone', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolumeHeadphone', -1) != -1 )) { + $SONOS_RenderingSubscriptions{$udn} = $renderingService->subscribe(\&SONOS_RenderingCallback); + $SONOS_ButtonPressQueue{$udn} = Thread::Queue->new(); + if (defined($SONOS_RenderingSubscriptions{$udn})) { + SONOS_Log undef, 2, 'Rendering-Service-subscribing successful with SID="'.$SONOS_RenderingSubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_RenderingSubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'Rendering-Service-subscribing NOT successful'; + } + } else { + undef($SONOS_RenderingSubscriptions{$udn}); + } + + # Alarm-Subscription + if ($alarmService && (SONOS_Client_Data_Retreive($udn, 'attr', 'getAlarms', 0) != 0)) { + $SONOS_AlarmSubscriptions{$udn} = $alarmService->subscribe(\&SONOS_AlarmCallback); + if (defined($SONOS_AlarmSubscriptions{$udn})) { + SONOS_Log undef, 2, 'Alarm-Service-subscribing successful with SID="'.$SONOS_AlarmSubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_AlarmSubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'Alarm-Service-subscribing NOT successful'; + } + } else { + undef($SONOS_AlarmSubscriptions{$udn}); + } + + # ZoneGroupTopology-Subscription + if ($zoneGroupTopologyService) { + $SONOS_ZoneGroupTopologySubscriptions{$udn} = $zoneGroupTopologyService->subscribe(\&SONOS_ZoneGroupTopologyCallback); + if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn})) { + SONOS_Log undef, 2, 'ZoneGroupTopology-Service-subscribing successful with SID="'.$SONOS_ZoneGroupTopologySubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_ZoneGroupTopologySubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'ZoneGroupTopology-Service-subscribing NOT successful'; + } + } else { + undef($SONOS_ZoneGroupTopologySubscriptions{$udn}); + } + + # DeviceProperties-Subscription + if ($devicePropertiesService) { + $SONOS_DevicePropertiesSubscriptions{$udn} = $devicePropertiesService->subscribe(\&SONOS_DevicePropertiesCallback); + if (defined($SONOS_DevicePropertiesSubscriptions{$udn})) { + SONOS_Log undef, 2, 'DeviceProperties-Service-subscribing successful with SID="'.$SONOS_DevicePropertiesSubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_DevicePropertiesSubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'DeviceProperties-Service-subscribing NOT successful'; + } + } else { + undef($SONOS_DevicePropertiesSubscriptions{$udn}); + } + + # AudioIn-Subscription + if ($audioInService) { + $SONOS_AudioInSubscriptions{$udn} = $audioInService->subscribe(\&SONOS_AudioInCallback); + if (defined($SONOS_AudioInSubscriptions{$udn})) { + SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID="'.$SONOS_AudioInSubscriptions{$udn}->SID.'" and Timeout="'.$SONOS_AudioInSubscriptions{$udn}->timeout.'s"'; + } else { + SONOS_Log undef, 1, 'AudioIn-Service-subscribing NOT successful'; + } + } else { + undef($SONOS_AudioInSubscriptions{$udn}); + } + + SONOS_Log undef, 3, 'Discover: End of discover-event for "'.$roomName.'".'; + } elsif ($action eq 'deviceRemoved') { + my $udn = $device->UDN; + $udn =~ s/.*?://i; + $udn .= '_MR'; + + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared'); + SONOS_Log undef, 2, "Device '$udn' removed. Do nothing special here, cause all is done in another way..."; + } + + return 0; +} + +######################################################################################## +# +# SONOS_IsAlive - Checks if the given Device is alive or not and triggers the proper event if status changed +# +# Parameter $udn = UDN of the Device in short-form (e.g. RINCON_000E5828D0F401400_MR) +# +######################################################################################## +sub SONOS_IsAlive($) { + my ($udn) = @_; + + SONOS_Log $udn, 4, "IsAlive-Event UDN=$udn"; + my $result = 1; + my $doDeleteProxyObjects = 0; + + $SONOS_Client_SendQueue_Suspend = 1; + + my $location = SONOS_Client_Data_Retreive($udn, 'reading', 'location', ''); + if ($location) { + SONOS_Log $udn, 5, "Location: $location"; + my $host = ($1) if ($location =~ m/http:\/\/(.*?):/); + + my $pingType = $SONOS_Client_Data{pingType}; + return 1 if (lc($pingType) eq 'none'); + if ($pingType ~~ @SONOS_PINGTYPELIST) { + SONOS_Log $udn, 5, "PingType: $pingType"; + } else { + SONOS_Log $udn, 1, "Wrong pingType given for '$udn': '$pingType'. Choose one of '".join(', ', @SONOS_PINGTYPELIST)."'"; + $pingType = $SONOS_DEFAULTPINGTYPE; + } + + my $ping = Net::Ping->new($pingType, 1); + if ($ping->ping($host)) { + # Alive + SONOS_Log $udn, 4, "$host is alive"; + $result = 1; + + # IsAlive-Negativ-Counter zurücksetzen + $SONOS_Thread_IsAlive_Counter{$host} = 0; + } else { + # Not Alive + $SONOS_Thread_IsAlive_Counter{$host}++; + + if ($SONOS_Thread_IsAlive_Counter{$host} > $SONOS_Thread_IsAlive_Counter_MaxMerci) { + SONOS_Log $udn, 3, "$host is REALLY NOT alive (out of merci maxlevel '".$SONOS_Thread_IsAlive_Counter_MaxMerci.'\')'; + $result = 0; + + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared'); + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'state', 'disappeared'); + $doDeleteProxyObjects = 1; + } else { + SONOS_Log $udn, 3, "$host is NOT alive, but in merci level ".$SONOS_Thread_IsAlive_Counter{$host}.'/'.$SONOS_Thread_IsAlive_Counter_MaxMerci.'.'; + } + } + $ping->close(); + } + + $SONOS_Client_SendQueue_Suspend = 0; + + # Jetzt, wo das Reading dazu auch gesetzt wurde, hier ausführen + if ($doDeleteProxyObjects) { + my %data; + $data{WorkType} = 'deleteProxyObjects'; + $data{UDN} = $udn; + my @params = (); + $data{Params} = \@params; + + $SONOS_ComObjectTransportQueue->enqueue(\%data); + + # Signalhandler aufrufen, wenn er nicht sowieso noch läuft... + if (defined(threads->object($SONOS_Thread))) { + threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1); + } + } + + return $result; +} + +######################################################################################## +# +# SONOS_DeleteProxyObjects - Deletes all references to the proxy objects of the given zoneplayer +# +# Parameter $name = The name of zoneplayerdevice +# +######################################################################################## +sub SONOS_DeleteProxyObjects($) { + my ($udn) = @_; + + SONOS_Log $udn, 2, "DeleteProxyObjects for '$udn'"; + + delete $SONOS_AVTransportControlProxy{$udn}; + delete $SONOS_RenderingControlProxy{$udn}; + delete $SONOS_ContentDirectoryControlProxy{$udn}; + delete $SONOS_AlarmClockControlProxy{$udn}; + delete $SONOS_AudioInProxy{$udn}; + delete $SONOS_DevicePropertiesProxy{$udn}; + delete $SONOS_GroupManagementProxy{$udn}; + delete $SONOS_MusicServicesProxy{$udn}; + delete $SONOS_ZoneGroupTopologyProxy{$udn}; + + delete $SONOS_TransportSubscriptions{$udn}; + + delete $SONOS_RenderingSubscriptions{$udn}; + + SONOS_Log $udn, 2, "DeleteProxyObjects DONE for '$udn'"; +} + +######################################################################################## +# +# SONOS_GetReadingsToCurrentHash - Get all neccessary readings from named device +# +# Parameter $name = The name of the player-device +# +######################################################################################## +sub SONOS_GetReadingsToCurrentHash($$) { + my ($name, $emptyCurrent) = @_; + + my %current; + + if ($emptyCurrent) { + # Empty Values for Current Track Readings + $current{TransportState} = 'ERROR'; + $current{Shuffle} = 0; + $current{Repeat} = 0; + $current{CrossfadeMode} = 0; + $current{NumberOfTracks} = ''; + $current{Track} = ''; + $current{TrackURI} = ''; + $current{TrackDuration} = ''; + $current{TrackPosition} = ''; + $current{TrackMetaData} = ''; + $current{AlbumArtURI} = ''; + $current{AlbumArtURL} = ''; + $current{Title} = ''; + $current{Artist} = ''; + $current{Album} = ''; + $current{OriginalTrackNumber} = ''; + $current{AlbumArtist} = ''; + $current{Sender} = ''; + $current{SenderCurrent} = ''; + $current{SenderInfo} = ''; + $current{nextTrackDuration} = ''; + $current{nextTrackURI} = ''; + $current{nextAlbumArtURI} = ''; + $current{nextAlbumArtURL} = ''; + $current{nextTitle} = ''; + $current{nextArtist} = ''; + $current{nextAlbum} = ''; + $current{nextAlbumArtist} = ''; + $current{nextOriginalTrackNumber} = ''; + $current{InfoSummarize1} = ''; + $current{InfoSummarize2} = ''; + $current{InfoSummarize3} = ''; + $current{InfoSummarize4} = ''; + $current{StreamAudio} = 0; + $current{NormalAudio} = 0; + } else { + # Insert normal Current Track Readings + $current{TransportState} = ReadingsVal($name, 'transportState', 'ERROR'); + $current{Shuffle} = ReadingsVal($name, 'Shuffle', 0); + $current{Repeat} = ReadingsVal($name, 'Repeat', 0); + $current{CrossfadeMode} = ReadingsVal($name, 'CrossfadeMode', 0); + $current{NumberOfTracks} = ReadingsVal($name, 'numberOfTracks', ''); + $current{Track} = ReadingsVal($name, 'currentTrack', ''); + $current{TrackURI} = ReadingsVal($name, 'currentTrackURI', ''); + $current{TrackDuration} = ReadingsVal($name, 'currentTrackDuration', ''); + $current{TrackPosition} = ReadingsVal($name, 'currentTrackPosition', ''); + #$current{TrackMetaData} = ''; + $current{AlbumArtURI} = ReadingsVal($name, 'currentAlbumArtURI', ''); + $current{AlbumArtURL} = ReadingsVal($name, 'currentAlbumArtURL', ''); + $current{Title} = ReadingsVal($name, 'currentTitle', ''); + $current{Artist} = ReadingsVal($name, 'currentArtist', ''); + $current{Album} = ReadingsVal($name, 'currentAlbum', ''); + $current{OriginalTrackNumber} = ReadingsVal($name, 'currentOriginalTrackNumber', ''); + $current{AlbumArtist} = ReadingsVal($name, 'currentAlbumArtist', ''); + $current{Sender} = ReadingsVal($name, 'currentSender', ''); + $current{SenderCurrent} = ReadingsVal($name, 'currentSenderCurrent', ''); + $current{SenderInfo} = ReadingsVal($name, 'currentSenderInfo', ''); + $current{nextTrackDuration} = ReadingsVal($name, 'nextTrackDuration', ''); + $current{nextTrackURI} = ReadingsVal($name, 'nextTrackURI', ''); + $current{nextAlbumArtURI} = ReadingsVal($name, 'nextAlbumArtURI', ''); + $current{nextAlbumArtURL} = ReadingsVal($name, 'nextAlbumArtURL', ''); + $current{nextTitle} = ReadingsVal($name, 'nextTitle', ''); + $current{nextArtist} = ReadingsVal($name, 'nextArtist', ''); + $current{nextAlbum} = ReadingsVal($name, 'nextAlbum', ''); + $current{nextAlbumArtist} = ReadingsVal($name, 'nextAlbumArtist', ''); + $current{nextOriginalTrackNumber} = ReadingsVal($name, 'nextOriginalTrackNumber', ''); + $current{InfoSummarize1} = ReadingsVal($name, 'infoSummarize1', ''); + $current{InfoSummarize2} = ReadingsVal($name, 'infoSummarize2', ''); + $current{InfoSummarize3} = ReadingsVal($name, 'infoSummarize3', ''); + $current{InfoSummarize4} = ReadingsVal($name, 'infoSummarize4', ''); + $current{StreamAudio} = ReadingsVal($name, 'currentStreamAudio', 0); + $current{NormalAudio} = ReadingsVal($name, 'currentNormalAudio', 0); + } + + # Insert Variables scanned during Device Detection or other events (for simple Replacing-Option of InfoSummarize) + $current{Volume} = ReadingsVal($name, 'Volume', 0); + $current{Mute} = ReadingsVal($name, 'Mute', 0); + $current{Balance} = ReadingsVal($name, 'Balance', 0); + $current{HeadphoneConnected} = ReadingsVal($name, 'HeadphoneConnected', 0); + $current{SleepTimer} = ReadingsVal($name, 'SleepTimer', ''); + $current{AlarmRunning} = ReadingsVal($name, 'AlarmRunning', ''); + $current{AlarmRunningID} = ReadingsVal($name, 'AlarmRunningID', ''); + $current{Presence} = ReadingsVal($name, 'presence', ''); + $current{RoomName} = ReadingsVal($name, 'roomName', ''); + $current{SaveRoomName} = ReadingsVal($name, 'saveRoomName', ''); + $current{PlayerType} = ReadingsVal($name, 'playerType', ''); + $current{Location} = ReadingsVal($name, 'location', ''); + $current{SoftwareRevision} = ReadingsVal($name, 'softwareRevision', ''); + $current{SerialNum} = ReadingsVal($name, 'serialNum', ''); + $current{ZoneGroupID} = ReadingsVal($name, 'ZoneGroupID', ''); + $current{ZoneGroupName} = ReadingsVal($name, 'ZoneGroupName', ''); + $current{ZonePlayerUUIDsInGroup} = ReadingsVal($name, 'ZonePlayerUUIDsInGroup', ''); + + return %current; +} + +######################################################################################## +# +# SONOS_ServiceCallback - Service-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_ServiceCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'Transport-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 4, "Transport-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received Transport-Event for Zone "'.$name.'".'; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AVTransport:1') { + SONOS_Log $udn, 1, 'Transport-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + # Check if the Variable called LastChange exists + if (not defined($properties{LastChange})) { + SONOS_Log $udn, 1, 'Transport-Event receive error: Property \'LastChange\' does not exists!'; + return; + } + + SONOS_Log $udn, 4, "Transport-Event: All correct with this service-call till now. UDN='uuid:$udn'"; + $SONOS_Client_SendQueue_Suspend = 1; + + # Determine the base URLs for downloading things from player + my $groundURL = ($1) if ($service->base =~ m/(http:\/\/.*?:\d+)/i); + SONOS_Log $udn, 4, "Transport-Event: GroundURL: $groundURL"; + + # Variablen initialisieren + SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':1'); + + my $currentValue; + + # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden... + $properties{LastChangeDecoded} = decode_entities($properties{LastChange}); + $properties{LastChangeDecoded} =~ s/[\r\n]//isg; # Komischerweise können hier unmaskierte Newlines auftauchen... wegmachen + + # Verarbeitung starten + SONOS_Log $udn, 4, 'Transport-Event: LastChange: '.$properties{LastChangeDecoded}; + + + # Bulkupdate hier starten... + #SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn); + + # Check, if this is a SleepTimer-Event + my $sleepTimerVersion = $1 if ($properties{LastChangeDecoded} =~ m/<r:SleepTimerGeneration val="(.*?)"\/>/i); + if (defined($sleepTimerVersion) && $sleepTimerVersion ne SONOS_Client_Data_Retreive($udn, 'reading', 'SleepTimerVersion', '')) { + # Variablen neu initialisieren, und die Original-Werte wieder mit reinholen + SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0'); + + # Neuer SleepTimer da! + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $result = $SONOS_AVTransportControlProxy{$udn}->GetRemainingSleepTimerDuration(); + my $currentValue = $result->getValue('RemainingSleepTimerDuration'); + $currentValue = '' if (!defined($currentValue)); + + # Wenn der Timer abgelaufen ist, wird nur ein Leerstring übergeben. Diesen durch das Wort off ersetzen. + $currentValue = 'off' if ($currentValue eq ''); + + SONOS_Client_Notifier('SetCurrent:SleepTimer:'.$currentValue); + + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'SleepTimerVersion', ($result->getValue('CurrentSleepTimerGeneration') ? $result->getValue('CurrentSleepTimerGeneration') : '')); + } + } + + # Um einen XML-Parser zu vermeiden, werden hier einige reguläre Ausdrücke für die Ermittlung der Werte eingesetzt... + # Transportstate ermitteln + if ($properties{LastChangeDecoded} =~ m/<TransportState val="(.*?)"\/>/i) { + $currentValue = decode_entities($1); + # Wenn der TransportState den neuen Wert 'Transitioning' hat, dann diesen auf Playing umsetzen, da das hier ausreicht. + $currentValue = 'PLAYING' if $currentValue eq 'TRANSITIONING'; + SONOS_Client_Notifier('SetCurrent:TransportState:'.$currentValue); + } + + # Wird hier gerade eine Alarm-Abspielung durchgeführt (oder beendet)? + SONOS_Client_Notifier('SetCurrent:AlarmRunning:'.$1) if ($properties{LastChangeDecoded} =~ m/<r:AlarmRunning val="(.*?)"\/>/i); + + # Wenn ein Alarm läuft, dann zusätzliche Informationen besorgen, ansonsten das entsprechende Reading leeren + if (defined($1) && $1 eq '1') { + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $alarmID = $SONOS_AVTransportControlProxy{$udn}->GetRunningAlarmProperties(0)->getValue('AlarmID'); + SONOS_Client_Notifier('SetCurrent:AlarmRunningID:'.$alarmID); + } + } elsif (defined($1) && $1 eq '0') { + SONOS_Client_Notifier('SetCurrent:AlarmRunningID:'); + } + + # Das nächste nur machen, wenn dieses Event die Track-Informationen auch enthält + if ($properties{LastChangeDecoded} =~ m/<TransportState val=".*?"\/>/i) { + # PlayMode ermitteln + my $currentPlayMode = 'NORMAL'; + $currentPlayMode = $1 if ($properties{LastChangeDecoded} =~ m/<CurrentPlayMode.*?val="(.*?)".*?\/>/i); + SONOS_Client_Notifier('SetCurrent:Shuffle:1') if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'SHUFFLE_NOREPEAT'); + SONOS_Client_Notifier('SetCurrent:Repeat:1') if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'REPEAT_ALL'); + + # CrossfadeMode ermitteln + SONOS_Client_Notifier('SetCurrent:CrossfadeMode:'.$1) if ($properties{LastChangeDecoded} =~ m/<CurrentCrossfadeMode.*?val="(\d+)".*?\/>/i); + + # Anzahl Tracknumber ermitteln + SONOS_Client_Notifier('SetCurrent:NumberOfTracks:'.decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<NumberOfTracks val="(.*?)"\/>/i); + + # Current Tracknumber ermitteln + SONOS_Client_Notifier('SetCurrent:Track:'.decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<CurrentTrack val="(.*?)"\/>/i); + + + # Current TrackURI ermitteln + my $currentTrackURI = SONOS_GetURIFromQueueValue($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackURI val="(.*?)"\/>/i); + SONOS_Client_Notifier('SetCurrent:TrackURI:'.$currentTrackURI); + + # Wenn es ein Spotify-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat + if ($currentTrackURI =~ m/^x-sonos-spotify:/i) { + my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/r:EnqueuedTransportURIMetaData val="(.*?)"\/>/i); + SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Spotify:'.uri_escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i); + } + + # Wenn es ein Napster/Rhapsody-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat + if ($currentTrackURI =~ m/^npsdy:/i) { + my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/r:EnqueuedTransportURIMetaData val="(.*?)"\/>/i); + SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Napster:'.uri_escape($1)) if ($enqueuedTransportMetaData =~ m/<desc .*?>(SA_.*?)<\/desc>/i); + } + + # Current Trackdauer ermitteln + SONOS_Client_Notifier('SetCurrent:TrackDuration:'.decode_entities($1)) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackDuration val="(.*?)"\/>/i); + + # Current Track Metadaten ermitteln + my $currentTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<CurrentTrackMetaData val="(.*?)"\/>/is); + SONOS_Log $udn, 4, 'Transport-Event: CurrentTrackMetaData: '.$currentTrackMetaData; + + # Cover herunterladen (Infos dazu in den Track Metadaten) + my $tempURIground = decode_entities($currentTrackMetaData); + $tempURIground =~ s/%25/%/ig; + + my $tempURI = ''; + $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i); + SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.$tempURI.':'.$groundURL); + + # Auch hier den XML-Parser verhindern, und alles per regulärem Ausdruck ermitteln... + if ($currentTrackMetaData =~ m/<dc:title>x-(sonosapi|rincon)-stream:.*?<\/dc:title>/) { + # Wenn es ein Stream ist, dann muss da was anderes erkannt werden + SONOS_Log $udn, 4, "Transport-Event: Stream erkannt!"; + SONOS_Client_Notifier('SetCurrent:StreamAudio:1'); + + # Sender ermitteln (per SOAP-Request an den SonosPlayer) + SONOS_Client_Notifier('SetCurrent:Sender:'.$1) if ($service->controlProxy()->GetMediaInfo(0)->getValue('CurrentURIMetaData') =~ m/<dc:title>(.*?)<\/dc:title>/i); + + # Sender-Läuft ermitteln + SONOS_Client_Notifier('SetCurrent:SenderCurrent:'.$1) if ($currentTrackMetaData =~ m/<r:radioShowMd>(.*?),p\d{6}<\/r:radioShowMd>/i); + + # Sendungs-Informationen ermitteln + $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:streamContent>(.*?)<\/r:streamContent>/i); + # Wenn hier eine Buffering- oder Connecting-Konstante zurückkommt, dann durch vernünftigen Text ersetzen + $currentValue = 'Verbindung herstellen...' if ($currentValue eq 'ZPSTR_CONNECTING'); + $currentValue = 'Wird gestartet...' if ($currentValue eq 'ZPSTR_BUFFERING'); + # Wenn hier RTL.it seine Infos liefert, diese zurechtschnippeln... + $currentValue = '' if ($currentValue eq '<songInfo />'); + if ($currentValue =~ m/<class>Music<\/class>.*?<mus_art_name>(.*?)<\/mus_art_name>/i) { + $currentValue = $1; + $currentValue =~ s/\[e\]amp\[p\]/&/ig; + } + SONOS_Client_Notifier('SetCurrent:SenderInfo:'.encode_entities($currentValue)); + } else { + SONOS_Log $udn, 4, "Transport-Event: Normal erkannt!"; + SONOS_Client_Notifier('SetCurrent:NormalAudio:1'); + + my $currentArtist = ''; + if ($currentTrackURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) { + # Gruppenwiedergabe feststellen, und dann andere Informationen anzeigen + SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)); + SONOS_Client_Notifier('SetCurrent:Title:Gruppenwiedergabe'); + SONOS_Client_Notifier('SetCurrent:Artist:'); + } elsif ($currentTrackURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) { + # LineIn-Wiedergabe feststellen, und dann andere Informationen anzeigen + SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)); + SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_replaceSpecialStringCharacters(decode_entities($1))) if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i); + SONOS_Client_Notifier('SetCurrent:Artist:'); + + SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_default.jpg:'); + } elsif ($currentTrackURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) { + # LineIn-Wiedergabe der Playbar feststellen, und dann andere Informationen anzeigen + SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)); + SONOS_Client_Notifier('SetCurrent:Title:SPDIF-Wiedergabe'); + SONOS_Client_Notifier('SetCurrent:Artist:'); + + SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_tv.jpg:'); + } else { + # Titel ermitteln + SONOS_Client_Notifier('SetCurrent:Title:'.$1) if ($currentTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i); + + # Interpret ermitteln + if ($currentTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i) { + $currentArtist = decode_entities($1); + SONOS_Client_Notifier('SetCurrent:Artist:'.encode_entities($currentArtist)); + } + + # Album ermitteln + SONOS_Client_Notifier('SetCurrent:Album:'.$1) if ($currentTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i); + } + + # Original Tracknumber ermitteln + SONOS_Client_Notifier('SetCurrent:OriginalTrackNumber:'.decode_entities($1)) if ($currentTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i); + + # Album Artist ermitteln + $currentValue = decode_entities($1) if ($currentTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i); + $currentValue = $currentArtist if ($currentValue eq ''); + SONOS_Client_Notifier('SetCurrent:AlbumArtist:'.encode_entities($currentValue)); + } + + # Next Track Metadaten ermitteln + my $nextTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m/<r:NextTrackMetaData val="(.*?)"\/>/i); + SONOS_Log $udn, 4, 'Transport-Event: NextTrackMetaData: '.$nextTrackMetaData; + + SONOS_Client_Notifier('SetCurrent:nextTrackDuration:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<res.*?duration="(.*?)".*?>/i); + + SONOS_Client_Notifier('SetCurrent:nextTrackURI:'.SONOS_GetURIFromQueueValue($1)) if ($properties{LastChangeDecoded} =~ m/<r:NextTrackURI val="(.*?)"\/>/i); + + $tempURIground = decode_entities($nextTrackMetaData); + $tempURIground =~ s/%25/%/ig; + + $tempURI = ''; + $tempURI = ($1) if ($tempURIground =~ m/<upnp:albumArtURI>(.*?)<\/upnp:albumArtURI>/i); + SONOS_Client_Notifier('ProcessCover:'.$udn.':1:'.$tempURI.':'.$groundURL); + + SONOS_Client_Notifier('SetCurrent:nextTitle:'.$1) if ($nextTrackMetaData =~ m/<dc:title>(.*?)<\/dc:title>/i); + + SONOS_Client_Notifier('SetCurrent:nextArtist:'.$1) if ($nextTrackMetaData =~ m/<dc:creator>(.*?)<\/dc:creator>/i); + + SONOS_Client_Notifier('SetCurrent:nextAlbum:'.$1) if ($nextTrackMetaData =~ m/<upnp:album>(.*?)<\/upnp:album>/i); + + SONOS_Client_Notifier('SetCurrent:nextAlbumArtist:'.$1) if ($nextTrackMetaData =~ m/<r:albumArtist>(.*?)<\/r:albumArtist>/i); + + SONOS_Client_Notifier('SetCurrent:nextOriginalTrackNumber:'.decode_entities($1)) if ($nextTrackMetaData =~ m/<upnp:originalTrackNumber>(.*?)<\/upnp:originalTrackNumber>/i); + } + + # Current Trackposition ermitteln (durch Abfrage beim Player) + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + my $trackPosition = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime'); + if ($trackPosition !~ /\d+:\d+:\d+/i) { # e.g. NOT_IMPLEMENTED + $trackPosition = '0:00:00'; + } + SONOS_Client_Notifier('SetCurrent:TrackPosition:'.$trackPosition); + } + + # Trigger/Transfer the whole bunch and generate InfoSummarize + SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn); + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of Transport-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_RenderingCallback - Rendering-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_RenderingCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'Rendering-Event receive error: SonosPlayer not found; Searching for \''.$service->eventSubURL.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 3, "Rendering-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received Rendering-Event for Zone "'.$name.'".'; + $SONOS_Client_SendQueue_Suspend = 1; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:RenderingControl:1') { + SONOS_Log $udn, 1, 'Rendering-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + # Check if the Variable called LastChange exists + if (not defined($properties{LastChange})) { + SONOS_Log $udn, 1, 'Rendering-Event receive error: Property \'LastChange\' does not exists!'; + return; + } + + SONOS_Log $udn, 4, "Rendering-Event: All correct with this service-call till now. UDN='uuid:".$udn."'"; + + # Die Daten wurden uns HTML-Kodiert übermittelt... diese Entities nun in Zeichen umwandeln, da sonst die regulären Ausdrücke ziemlich unleserlich werden... + $properties{LastChangeDecoded} = decode_entities($properties{LastChange}); + + SONOS_Log $udn, 4, 'Rendering-Event: LastChange: '.$properties{LastChangeDecoded}; + my $generateVolumeEvent = SONOS_Client_Data_Retreive($udn, 'attr', 'generateVolumeEvent', 0); + + # Mute? + my $mute = SONOS_Client_Data_Retreive($udn, 'reading', 'Mute', 0); + if ($properties{LastChangeDecoded} =~ m/<Mute.*?channel="Master".*?val="(\d+)".*?\/>/i) { + SONOS_AddToButtonQueue($udn, 'M') if ($1 ne $mute); + $mute = $1; + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Mute', $mute); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Mute', $mute); + } + } + + # Headphone? + my $headphoneConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'HeadphoneConnected', 0); + if ($properties{LastChangeDecoded} =~ m/<HeadphoneConnected.*?val="(\d+)".*?\/>/i) { + SONOS_AddToButtonQueue($udn, 'H') if ($1 ne $headphoneConnected); + $headphoneConnected = $1; + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'HeadphoneConnected', $headphoneConnected); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'HeadphoneConnected', $headphoneConnected); + } + } + + + # Balance ermitteln + my $balance = SONOS_Client_Data_Retreive($udn, 'reading', 'Balance', 0); + if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="LF".*?val="(\d+)".*?\/>/i) { + my $volumeLeft = $1; + my $volumeRight = $1 if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="RF".*?val="(\d+)".*?\/>/i); + $balance = (-$volumeLeft) + $volumeRight if ($volumeLeft && $volumeRight); + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Balance', $balance); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Balance', $balance); + } + } + + + # Volume ermitteln + my $currentVolume = SONOS_Client_Data_Retreive($udn, 'reading', 'Volume', 0); + if ($properties{LastChangeDecoded} =~ m/<Volume.*?channel="Master".*?val="(\d+)".*?\/>/i) { + SONOS_AddToButtonQueue($udn, 'U') if ($1 > $currentVolume); + SONOS_AddToButtonQueue($udn, 'D') if ($1 < $currentVolume); + $currentVolume = $1 ; + } + + # Loudness? + my $loudness = SONOS_Client_Data_Retreive($udn, 'reading', 'Loudness', 0); + if ($properties{LastChangeDecoded} =~ m/<Loudness.*?channel="Master".*?val="(\d+)".*?\/>/i) { + $loudness = $1; + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Loudness', $loudness); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Loudness', $loudness); + } + } + + # Bass? + my $bass = SONOS_Client_Data_Retreive($udn, 'reading', 'Bass', 0); + if ($properties{LastChangeDecoded} =~ m/<Bass.*?val="([-]{0,1}\d+)".*?\/>/i) { + $bass = $1; + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Bass', $bass); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Bass', $bass); + } + } + + # Treble? + my $treble = SONOS_Client_Data_Retreive($udn, 'reading', 'Treble', 0); + if ($properties{LastChangeDecoded} =~ m/<Treble.*?val="([-]{0,1}\d+)".*?\/>/i) { + $treble = $1; + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Treble', $treble); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Treble', $treble); + } + } + + + SONOS_Log $udn, 4, "Rendering-Event: Current Values for '$name' ~ Volume: $currentVolume, HeadphoneConnected: $headphoneConnected, Bass: $bass, Treble: $treble, Balance: $balance, Loudness: $loudness, Mute: $mute"; + + # Grenzen passend zum verwendeten Tonausgang ermitteln + # Untere Grenze ermitteln + my $key = 'minVolume'.($headphoneConnected ? 'Headphone' : ''); + my $minVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 0); + + # Obere Grenze ermitteln + $key = 'maxVolume'.($headphoneConnected ? 'Headphone' : ''); + my $maxVolume = SONOS_Client_Data_Retreive($udn, 'attr', $key, 100); + + SONOS_Log $udn, 4, "Rendering-Event: Current Borders for '$name' ~ minVolume: $minVolume, maxVolume: $maxVolume"; + + + # Fehlerhafte Attributangaben? + if ($minVolume > $maxVolume) { + SONOS_Log $udn, 0, 'Min-/MaxVolume check Error: MinVolume('.$minVolume.') > MaxVolume('.$maxVolume.'), using Headphones: '.$headphoneConnected.'!'; + return; + } + + # Prüfungen und Aktualisierungen durchführen + if (!$mute && ($minVolume > $currentVolume)) { + # Grenzen prüfen: Zu Leise + SONOS_Log $udn, 4, 'Volume to Low. Correct it to "'.$minVolume.'"'; + + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $minVolume); + } elsif (!$mute && ($currentVolume > $maxVolume)) { + # Grenzen prüfen: Zu Laut + SONOS_Log $udn, 4, 'Volume to High. Correct it to "'.$maxVolume.'"'; + + $SONOS_RenderingControlProxy{$udn}->SetVolume(0, 'Master', $maxVolume); + } else { + # Alles OK, nur im FHEM aktualisieren + if ($generateVolumeEvent) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'Volume', $currentVolume); + } else { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChangedNoTrigger', $udn, 'Volume', $currentVolume); + } + + # Variablen initialisieren + SONOS_Client_Notifier('GetReadingsToCurrentHash:'.$udn.':0'); + SONOS_Client_Notifier('CurrentBulkUpdate:'.$udn); + } + + # ButtonQueue prüfen + SONOS_CheckButtonQueue($udn); + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of Rendering-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_AddToButtonQueue - Adds the given Event-Name to the ButtonQueue +# +######################################################################################## +sub SONOS_AddToButtonQueue($$) { + my ($udn, $event) = @_; + + my $data = {Action => uc($event), Time => time()}; + $SONOS_ButtonPressQueue{$udn}->enqueue($data); +} + +######################################################################################## +# +# SONOS_CheckButtonQueue - Checks ButtonQueue and triggers events if neccessary +# +######################################################################################## +sub SONOS_CheckButtonQueue($) { + my ($udn) = @_; + + my $eventDefinitions = SONOS_Client_Data_Retreive($udn, 'attr', 'buttonEvents', ''); + + # Wenn keine Events definiert wurden, dann Queue einfach leeren und zurückkehren... + # Das beschleunigt die Verarbeitung, da im allgemeinen keine (oder eher wenig) Events definiert werden. + if (!$eventDefinitions) { + $SONOS_ButtonPressQueue{$udn}->dequeue_nb(10); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen... + return; + } + + my $maxElems = 0; + while ($eventDefinitions =~ m/(\d+):([MHUD]+)/g) { + $maxElems = SONOS_Max($maxElems, length($2)); + + # Sind überhaupt ausreichend Events in der Queue, das dieses ButtonEvent ausgefüllt sein könnte? + my $ok = $SONOS_ButtonPressQueue{$udn}->pending() >= length($2); + + # Prüfen, ob alle Events in der Queue der Reihenfolge des ButtonEvents entsprechen + if ($ok) { + for (my $i = 0; $i < length($2); $i++) { + if ($SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2) + $i)->{Action} ne substr($2, $i, 1)) { + $ok = 0; + } + } + } + + # Wenn die Kette stimmt, dann hier prüfen, ob die Maximalzeit eingehalten wurde, und dann u.U. das Event werfen... + if ($ok) { + if (time() - $SONOS_ButtonPressQueue{$udn}->peek($SONOS_ButtonPressQueue{$udn}->pending() - length($2))->{Time} <= $1) { + # Event here... + SONOS_Log $udn, 3, 'Generating ButtonEvent for Zone "'.$udn.'": '.$2.'.'; + SONOS_Client_Data_Refresh('ReadingsSingleUpdate', $udn, 'ButtonEvent', $2); + } + } + } + + # Einträge, die "zu viele Elemente" her sind, wieder entfernen, da diese sowieso keine Berücksichtigung mehr finden werden + if ($SONOS_ButtonPressQueue{$udn}->pending() > $maxElems) { + $SONOS_ButtonPressQueue{$udn}->extract(0, $SONOS_ButtonPressQueue{$udn}->pending() - $maxElems); # Es können pro Rendering-Event im Normalfall nur 4 Elemente dazukommen... + } +} + +######################################################################################## +# +# SONOS_AlarmCallback - Alarm-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_AlarmCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'Alarm-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 3, "Alarm-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received Alarm-Event for Zone "'.$name.'".'; + $SONOS_Client_SendQueue_Suspend = 1; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AlarmClock:1') { + SONOS_Log $udn, 1, 'Alarm-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + # Check if the Variable called AlarmListVersion or DailyIndexRefreshTime exists + if (!defined($properties{AlarmListVersion}) && !defined($properties{DailyIndexRefreshTime})) { + return; + } + + SONOS_Log $udn, 4, "Alarm-Event: All correct with this service-call till now. UDN='uuid:".$udn."'"; + + # If a new AlarmListVersion is available + my $alarmListVersion = SONOS_Client_Data_Retreive($udn, 'reading', 'AlarmListVersion', '~~'); + if (defined($properties{AlarmListVersion}) && ($properties{AlarmListVersion} ne $alarmListVersion)) { + SONOS_Log $udn, 4, 'Set new Alarm-Data'; + # Retrieve new AlarmList + my $result = $SONOS_AlarmClockControlProxy{$udn}->ListAlarms(); + + my $currentAlarmList = $result->getValue('CurrentAlarmList'); + my %alarms = (); + my @alarmIDs = (); + while ($currentAlarmList =~ m/<Alarm (.*?)\/>/gi) { + my $alarm = $1; + + # Nur die Alarme, die auch für diesen Raum gelten, reinholen... + if ($alarm =~ m/RoomUUID="$udnShort"/i) { + my $id = $1 if ($alarm =~ m/ID="(\d+)"/i); + SONOS_Log $udn, 5, 'Alarm-Event: Alarm: '.SONOS_Stringify($alarm); + + push @alarmIDs, $id; + + $alarms{$id}{StartTime} = $1 if ($alarm =~ m/StartTime="(.*?)"/i); + $alarms{$id}{Duration} = $1 if ($alarm =~ m/Duration="(.*?)"/i); + $alarms{$id}{Recurrence_Once} = 0; + $alarms{$id}{Recurrence_Monday} = 0; + $alarms{$id}{Recurrence_Tuesday} = 0; + $alarms{$id}{Recurrence_Wednesday} = 0; + $alarms{$id}{Recurrence_Thursday} = 0; + $alarms{$id}{Recurrence_Friday} = 0; + $alarms{$id}{Recurrence_Saturday} = 0; + $alarms{$id}{Recurrence_Sunday} = 0; + $alarms{$id}{Enabled} = $1 if ($alarm =~ m/Enabled="(.*?)"/i); + $alarms{$id}{RoomUUID} = $1 if ($alarm =~ m/RoomUUID="(.*?)"/i); + $alarms{$id}{ProgramURI} = decode_entities($1) if ($alarm =~ m/ProgramURI="(.*?)"/i); + $alarms{$id}{ProgramMetaData} = decode_entities($1) if ($alarm =~ m/ProgramMetaData="(.*?)"/i); + $alarms{$id}{Shuffle} = 0; + $alarms{$id}{Repeat} = 0; + $alarms{$id}{Volume} = $1 if ($alarm =~ m/Volume="(.*?)"/i); + $alarms{$id}{IncludeLinkedZones} = $1 if ($alarm =~ m/IncludeLinkedZones="(.*?)"/i); + + # PlayMode ermitteln... + my $currentPlayMode = 'NORMAL'; + $currentPlayMode = $1 if ($alarm =~ m/PlayMode="(.*?)"/i); + $alarms{$id}{Shuffle} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'SHUFFLE_NOREPEAT'); + $alarms{$id}{Repeat} = 1 if ($currentPlayMode eq 'SHUFFLE' || $currentPlayMode eq 'REPEAT_ALL'); + + # Recurrence ermitteln... + my $currentRecurrence = $1 if ($alarm =~ m/Recurrence="(.*?)"/i); + $alarms{$id}{Recurrence_Once} = 1 if ($currentRecurrence eq 'ONCE'); + $alarms{$id}{Recurrence_Monday} = 1 if ($currentRecurrence =~ m/^ON_\d*?1/i); + $alarms{$id}{Recurrence_Tuesday} = 1 if ($currentRecurrence =~ m/^ON_\d*?2/i); + $alarms{$id}{Recurrence_Wednesday} = 1 if ($currentRecurrence =~ m/^ON_\d*?3/i); + $alarms{$id}{Recurrence_Thursday} = 1 if ($currentRecurrence =~ m/^ON_\d*?4/i); + $alarms{$id}{Recurrence_Friday} = 1 if ($currentRecurrence =~ m/^ON_\d*?5/i); + $alarms{$id}{Recurrence_Saturday} = 1 if ($currentRecurrence =~ m/^ON_\d*?6/i); + $alarms{$id}{Recurrence_Sunday} = 1 if ($currentRecurrence =~ m/^ON_\d*?7/i); + + SONOS_Log $udn, 5, 'Alarm-Event: Alarm-Decoded: '.SONOS_Stringify(\%alarms); + } + } + + # Sets the approbriate Readings-Value + $Data::Dumper::Indent = 0; + # SONOS_Client_Notifier('SetAlarm:'.$udn.':'.$result->getValue('CurrentAlarmListVersion').';'.join(',', @alarmIDs).':'.Dumper(\%alarms)); + SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmList', Dumper(\%alarms)); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListIDs', join(',', @alarmIDs)); + SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'AlarmListVersion', $result->getValue('CurrentAlarmListVersion')); + SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn); + $Data::Dumper::Indent = 2; + } + + if (defined($properties{DailyIndexRefreshTime})) { + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'DailyIndexRefreshTime', $properties{DailyIndexRefreshTime}); + } + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of Alarm-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_ZoneGroupTopologyCallback - ZoneGroupTopology-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_ZoneGroupTopologyCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'ZoneGroupTopology-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 3, "ZoneGroupTopology-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received ZoneGroupTopology-Event for Zone "'.$name.'".'; + $SONOS_Client_SendQueue_Suspend = 1; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:ZoneGroupTopology:1') { + SONOS_Log $udn, 1, 'ZoneGroupTopology-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + SONOS_Log $udn, 4, "ZoneGroupTopology-Event: All correct with this service-call till now. UDN='uuid:".$udn."'"; + + # ZoneGroupState: Gesamtkonstellation + my $zoneGroupState = ''; + if ($properties{ZoneGroupState}) { + $zoneGroupState = decode_entities($1) if ($properties{ZoneGroupState} =~ m/(.*)/); + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', 'undef', 'ZoneGroupState', $zoneGroupState); + } + + # ZonePlayerUUIDsInGroup: Welche Player befinden sich alle in der gleichen Gruppe wie ich? + my $zonePlayerUUIDsInGroup = SONOS_Client_Data_Retreive($udn, 'reading', 'ZonePlayerUUIDsInGroup', ''); + if ($properties{ZonePlayerUUIDsInGroup}) { + $zonePlayerUUIDsInGroup = $properties{ZonePlayerUUIDsInGroup}; + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'ZonePlayerUUIDsInGroup', $zonePlayerUUIDsInGroup); + } + + # ZoneGroupID: Welcher Gruppe gehöre ich aktuell an, und hat sich meine Aufgabe innerhalb der Gruppe verändert? + my $zoneGroupID = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupID', ''); + my $fieldType = SONOS_Client_Data_Retreive($udn, 'reading', 'fieldType', ''); + if ($zoneGroupState =~ m/.*(<ZoneGroup Coordinator="(RINCON_[0-9a-f]+)".*?>).*?(<(ZoneGroupMember|Satellite) UUID="$udnShort".*?(>|\/>))/is) { + $zoneGroupID = $2; + my $member = $3; + + my $topoType = ''; + # Ist dieser Player in einem ChannelMapSet (also einer Paarung) enthalten? + if ($member =~ m/ChannelMapSet=".*?$udnShort:(.*?),(.*?)[;"]/is) { + $topoType = '_'.$1; + } + + # Ist dieser Player in einem HTSatChanMapSet (also einem Surround-System) enthalten? + if ($member =~ m/HTSatChanMapSet=".*?$udnShort:(.*?)[;"]/is) { + $topoType = '_'.$1; + $topoType =~ s/,/_/g; + } + + SONOS_Log undef, 4, 'Retrieved TopoType: '.$topoType; + $fieldType = substr($topoType, 1) if ($topoType ne ''); + } + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'ZoneGroupID', $zoneGroupID.':__'); + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'fieldType', $fieldType); + + # ZoneGroupName: Welchen Namen hat die aktuelle Gruppe? + my $zoneGroupName = SONOS_Client_Data_Retreive($udn, 'reading', 'ZoneGroupName', ''); + if ($properties{ZoneGroupName}) { + $zoneGroupName = $properties{ZoneGroupName}; + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'ZoneGroupName', $zoneGroupName); + } + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of ZoneGroupTopology-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_DevicePropertiesCallback - DeviceProperties-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_DevicePropertiesCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'DeviceProperties-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 3, "DeviceProperties-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received DeviceProperties-Event for Zone "'.$name.'".'; + $SONOS_Client_SendQueue_Suspend = 1; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:DeviceProperties:1') { + SONOS_Log $udn, 1, 'DeviceProperties-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + SONOS_Log $udn, 4, "DeviceProperties-Event: All correct with this service-call till now. UDN='uuid:".$udn."'"; + + # Raumname wurde angepasst? + my $roomName = SONOS_Client_Data_Retreive($udn, 'reading', 'roomName', ''); + if (defined($properties{ZoneName}) && $properties{ZoneName} ne '') { + $roomName = decode(SONOS_Client_Data_Retreive('undef', 'attr', 'characterDecoding', 'CP-1252'), $properties{ZoneName}); + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'roomName', $roomName); + + my $saveRoomName = decode('UTF-8', $roomName); + $saveRoomName =~ s/([äöüÄÖÜß])/SONOS_UmlautConvert($1)/eg; # Hier erstmal Umlaute 'schön' machen, damit dafür nicht '_' verwendet werden... + $saveRoomName =~ s/[^a-zA-Z0-9]/_/g; + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'saveRoomName', $saveRoomName); + } + + # Icon wurde angepasst? + my $roomIcon = SONOS_Client_Data_Retreive($udn, 'reading', 'roomIcon', ''); + if (defined($properties{Icon}) && $properties{Icon} ne '') { + $properties{Icon} =~ s/.*?:(.*)/$1/i; + + $roomIcon = $properties{Icon}; + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'roomIcon', $roomIcon); + } + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of DeviceProperties-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_AudioInCallback - AudioIn-Callback, +# +# Parameter $service = Service-Representing Object +# $properties = Properties, that have been changed in this event +# +######################################################################################## +sub SONOS_AudioInCallback($$) { + my ($service, %properties) = @_; + + my $udn = $SONOS_Locations{$service->base}; + my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i); + + if (!$udn) { + SONOS_Log undef, 1, 'AudioIn-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!'; + return; + } + + my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn); + + # If the Device is disabled, return here... + if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) { + SONOS_Log $udn, 3, "AudioIn-Event: device '$name' disabled. No Events/Data will be processed!"; + return; + } + + SONOS_Log $udn, 3, 'Event: Received AudioIn-Event for Zone "'.$name.'".'; + $SONOS_Client_SendQueue_Suspend = 1; + + # Check if the correct ServiceType + if ($service->serviceType() ne 'urn:schemas-upnp-org:service:AudioIn:1') { + SONOS_Log $udn, 1, 'AudioIn-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!'; + return; + } + + SONOS_Log $udn, 4, "AudioIn-Event: All correct with this service-call till now. UDN='uuid:".$udn."'"; + + # LineInConnected wurde angepasst? + my $lineInConnected = SONOS_Client_Data_Retreive($udn, 'reading', 'LineInConnected', ''); + if (defined($properties{LineInConnected}) && $properties{LineInConnected} ne '') { + $lineInConnected = $properties{LineInConnected}; + SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'LineInConnected', $lineInConnected); + } + + $SONOS_Client_SendQueue_Suspend = 0; + SONOS_Log $udn, 3, 'Event: End of AudioIn-Event for Zone "'.$name.'".'; + + return 0; +} + +######################################################################################## +# +# SONOS_replaceSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal +# +# Parameter text = The text, inside that has to be searched and replaced +# +######################################################################################## +sub SONOS_replaceSpecialStringCharacters($) { + my ($text) = @_; + + $text =~ s/"/'/g; + + return $text; +} + +######################################################################################## +# +# SONOS_maskSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal +# +# Parameter text = The text, inside that has to be searched and replaced +# +######################################################################################## +sub SONOS_maskSpecialStringCharacters($) { + my ($text) = @_; + + $text =~ s/"/\\"/g; + + return $text; +} + +######################################################################################## +# +# SONOS_ProcessInfoSummarize - Process the InfoSummarize-Fields (XML-Alike Structure) +# Example for Minimal neccesary structure: +# <NormalAudio></NormalAudio> <StreamAudio></StreamAudio> +# +# Complex Example: +# <NormalAudio><Artist prefix="(" suffix=")"/><Title prefix=" '" suffix="'" ifempty="[Keine Musikdatei]"/><Album prefix=" vom Album '" suffix="'"/></NormalAudio> <StreamAudio><Sender suffix=":"/><SenderCurrent prefix=" '" suffix="'"/><SenderInfo prefix=" - "/></StreamAudio> +# OR +# <NormalAudio><TransportState/><InfoSummarize1 prefix=" => "/></NormalAudio> <StreamAudio><TransportState/><InfoSummarize1 prefix=" => "/></StreamAudio> +# +# Parameter name = The name of the SonosPlayer-Device +# current = The Current-Values hashset +# summarizeVariableName = The variable-name to process (e.g. "InfoSummarize1") +# +######################################################################################## +sub SONOS_ProcessInfoSummarize($$$$) { + my ($hash, $current, $summarizeVariableName, $bulkUpdate) = @_; + + if (($current->{$summarizeVariableName} = AttrVal($hash->{NAME}, 'generate'.$summarizeVariableName, '')) ne '') { + # Only pick up the current Audio-Type-Part, if one is available... + if ($current->{NormalAudio}) { + $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<NormalAudio>(.*?)<\/NormalAudio>/i); + } else { + $current->{$summarizeVariableName} = $1 if ($current->{$summarizeVariableName} =~ m/<StreamAudio>(.*?)<\/StreamAudio>/i); + } + + # Replace placeholder with variables (list defined in 21_SONOSPLAYER ~ stateVariable) + my $availableVariables = ($2) if (getAllAttr($hash->{NAME}) =~ m/(^|\s+)stateVariable:(.*?)(\s+|$)/); + foreach (split(/,/, $availableVariables)) { + $current->{$summarizeVariableName} = SONOS_ReplaceTextToken($current->{$summarizeVariableName}, $_, $current->{$_}); + } + + if ($bulkUpdate) { + # Enqueue the event + SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName}); + } else { + SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), $current->{$summarizeVariableName}, 1); + } + } else { + if ($bulkUpdate) { + # Enqueue the event + SONOS_readingsBulkUpdateIfChanged($hash, lcfirst($summarizeVariableName), ''); + } else { + SONOS_readingsSingleUpdateIfChanged($hash, lcfirst($summarizeVariableName), '', 1); + } + } +} + +######################################################################################## +# +# SONOS_ReplaceTextToken - Search and replace any occurency of the given tokenName with the value of tokenValue +# +# Parameter text = The text, inside that has to be searched and replaced +# tokenName = The name, that has to be searched for +# tokenValue = The value, the has to be insert instead of tokenName +# +######################################################################################## +sub SONOS_ReplaceTextToken($$$) { + my ($text, $tokenName, $tokenValue) = @_; + + # Hier das Token mit Prefix, Suffix, Instead und IfEmpty ersetzen, wenn entsprechend vorhanden + $text =~ s/<\s*?$tokenName(\s.*?\/|\/)>/SONOS_ReplaceTextTokenRegReplacer($tokenValue, $1)/eig; + + return $text; +} + +######################################################################################## +# +# SONOS_ReplaceTextTokenRegReplacer - Internal procedure for replacing TagValues +# +# Parameter tokenValue = The value, the has to be insert instead of tokenName +# $matcher = The values of the searched and found tag +# +######################################################################################## +sub SONOS_ReplaceTextTokenRegReplacer($$) { + my ($tokenValue, $matcher) = @_; + + my $emptyVal = SONOS_DealToken($matcher, 'emptyVal', ''); + + return SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'prefix', ''), $emptyVal). + SONOS_ReturnIfEmpty($tokenValue, SONOS_DealToken($matcher, 'ifempty', $emptyVal), $emptyVal). + SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'instead', $tokenValue), $emptyVal). + SONOS_ReturnIfNotEmpty($tokenValue, SONOS_DealToken($matcher, 'suffix', ''), $emptyVal); +} + +######################################################################################## +# +# SONOS_DealToken - Extracts the content of the given tokenName if exist in checkText +# +# Parameter checkText = The text, that has to be search in +# tokenName = The value, of which the content has to be returned +# +######################################################################################## +sub SONOS_DealToken($$$) { + my ($checkText, $tokenName, $emptyVal) = @_; + + my $returnText = $1 if($checkText =~ m/$tokenName\s*=\s*"(.*?)"/i); + + return $emptyVal if (not defined($returnText)); + return $returnText; +} + +######################################################################################## +# +# SONOS_ReturnIfEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is* empty +# +# Parameter checkText = The text, that has to be checked +# returnValue = The value, the has to be returned +# +######################################################################################## +sub SONOS_ReturnIfEmpty($$$) { + my ($checkText, $returnValue, $emptyVal) = @_; + + return '' if not defined($returnValue); + return $returnValue if ((not defined($checkText)) || $checkText eq $emptyVal); + return ''; +} + +######################################################################################## +# +# SONOS_ReturnIfNotEmpty - Returns the second Parameter returnValue only, if the first Parameter checkText *is NOT* empty +# +# Parameter checkText = The text, that has to be checked +# returnValue = The value, the has to be returned +# +######################################################################################## +sub SONOS_ReturnIfNotEmpty($$$) { + my ($checkText, $returnValue, $emptyVal) = @_; + + return '' if not defined($returnValue); + return $returnValue if (defined($checkText) && $checkText ne $emptyVal); + return ''; +} + +######################################################################################## +# +# SONOS_ImageDownloadTypeExtension - Gives the appropriate extension for the retrieved mimetype of the content of the given url +# +# Parameter url = The URL of the content +# +######################################################################################## +sub SONOS_ImageDownloadTypeExtension($) { + my ($url) = @_; + + # Wenn Spotify, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG + if ($url =~ m/x-sonos-spotify/) { + return 'jpg'; + } + + # Wenn Napster, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG + if ($url =~ m/npsdy/) { + return 'jpg'; + } + + # Wenn Radio, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer GIF + if ($url =~ m/x-sonosapi-stream/) { + return 'gif'; + } + + # Wenn Google Music oder Simfy, dann sendet der Zoneplayer keinen Mimetype, der ist dann immer JPG + if ($url =~ m/x-sonos-http/) { + return 'jpg'; + } + + # Server abfragen + my ($content_type, $document_length, $modified_time, $expires, $server) = head($url); + + return 'ERROR' if (!defined($content_type) || ($content_type =~ m/<head>.*?<\/head>/)); + + if ($content_type =~ m/png/) { + return 'png'; + } elsif (($content_type =~ m/jpeg/) || ($content_type =~ m/jpg/)) { + return 'jpg'; + } elsif ($content_type =~ m/gif/) { + return 'gif'; + } else { + $content_type =~ s/\//-/g; + return $content_type; + } +} + +######################################################################################## +# +# SONOS_ImageDownloadMimeType - Retrieves the mimetype of the content of the given url +# +# Parameter url = The URL of the content +# +######################################################################################## +sub SONOS_ImageDownloadMimeType($) { + my ($url) = @_; + + my ($content_type, $document_length, $modified_time, $expires, $server) = head($url); + + return $content_type; +} + +######################################################################################## +# +# SONOS_DownloadReplaceIfChanged - Overwrites the file only if its changed +# +# Parameter url = The URL of the new file +# dest = The local file-uri of the old file +# +# Return 1 = New file have been written +# 0 = nothing happened, because the filecontents are identical +# +######################################################################################## +sub SONOS_DownloadReplaceIfChanged($$) { + my ($url, $dest) = @_; + + # Reading new file + my $newFile = get $url; + + if (not defined($newFile)) { + SONOS_Log undef, 4, 'Couldn\'t retrieve file "'.$url.'" via web. Trying to copy directly...'; + + $newFile = SONOS_ReadFile($url); + if (not defined($newFile)) { + SONOS_Log undef, 4, 'Couldn\'t even copy file "'.$url.'" directly... exiting...'; + return 0; + } + } + + # Reading old file (if it exists) + my $oldFile = SONOS_ReadFile($dest); + $oldFile = '' if (!defined($oldFile)); + + # compare those files, and overwrite old file, if it has to be changed + if ($newFile ne $oldFile) { + # Hier jetzt alle Dateien dieses Players entfernen, damit nichts überflüssiges rumliegt, falls sich die Endung geändert haben sollte + if (($dest =~ m/(.*\.).*?/) && ($1 ne '')) { + unlink(<$1*>); + } + + # Hier jetzt die neue Datei herunterladen + SONOS_Log undef, 4, "New filecontent for '$dest'!"; + if (defined(open IMGFILE, '>'.$dest)) { + binmode IMGFILE ; + print IMGFILE $newFile; + close IMGFILE; + } else { + SONOS_Log undef, 1, "Error creating file $dest"; + } + + return 1; + } else { + SONOS_Log undef, 4, "Identical filecontent for '$dest'!"; + + return 0; + } +} + +######################################################################################## +# +# SONOS_ReadFile - Read the content of the given filename +# +# Parameter $fileName = The filename, that has to be read +# +######################################################################################## +sub SONOS_ReadFile($) { + my ($fileName) = @_; + + if (-e $fileName) { + my $fileContent = ''; + + open IMGFILE, '<'.$fileName; + binmode IMGFILE; + while (<IMGFILE>){ + $fileContent .= $_; + } + close IMGFILE; + + return $fileContent; + } + + return undef; +} + +######################################################################################## +# +# SONOS_WriteFile - Write the content to the given filename +# +# Parameter $fileName = The filename, that has to be read +# +######################################################################################## +sub SONOS_WriteFile($$) { + my ($fileName, $data) = @_; + + open IMGFILE, '>'.$fileName; + binmode IMGFILE; + print IMGFILE $data; + close IMGFILE; +} + +######################################################################################## +# +# SONOS_readingsBulkUpdateIfChanged - Wrapper for readingsBulkUpdate. Do only things if value has changed. +# +######################################################################################## +sub SONOS_readingsBulkUpdateIfChanged($$$) { + my ($hash, $readingName, $readingValue) = @_; + + return if (!defined($hash) || !defined($readingName) || !defined($readingValue)); + + readingsBulkUpdate($hash, $readingName, $readingValue) if ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue; +} + +######################################################################################## +# +# SONOS_readingsEndUpdate - Wrapper for readingsEndUpdate. +# +######################################################################################## +sub SONOS_readingsEndUpdate($$) { + my ($hash, $doTrigger) = @_; + + readingsEndUpdate($hash, $doTrigger); +} + +######################################################################################## +# +# SONOS_readingsSingleUpdateIfChanged - Wrapper for readingsSingleUpdate. Do only things if value has changed. +# +######################################################################################## +sub SONOS_readingsSingleUpdateIfChanged($$$$) { + my ($hash, $readingName, $readingValue, $doTrigger) = @_; + + readingsSingleUpdate($hash, $readingName, $readingValue, $doTrigger) if ReadingsVal($hash->{NAME}, $readingName, '~~ReAlLyNoTeQuAlSmArKeR~~') ne $readingValue; +} + +######################################################################################## +# +# SONOS_RefreshIconsInFHEMWEB - Refreshs Iconcache in all FHEMWEB-Instances +# +######################################################################################## +sub SONOS_RefreshIconsInFHEMWEB($) { + my ($dir) = @_; + $dir = $attr{global}{modpath}.$dir; + + foreach my $fhem_dev (sort keys %main::defs) { + if ($main::defs{$fhem_dev}{TYPE} eq 'FHEMWEB') { + eval('fhem(\'set '.$main::defs{$fhem_dev}{NAME}.' rereadicons\');'); + last; # Die Icon-Liste ist global, muss also nur einmal neu gelesen werden + } + } +} + +######################################################################################## +# +# SONOS_getAllSonosplayerDevices - Retreives all available/defined Sonosplayer-Devices +# +######################################################################################## +sub SONOS_getAllSonosplayerDevices() { + my @devices = (); + + foreach my $fhem_dev (sort keys %main::defs) { + push @devices, $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER'); + } + + return @devices; +} + +######################################################################################## +# +# SONOS_getDeviceDefHash - Retrieves the Def-Hash for the SONOS-Device (only one should exists, so this is OK) +# or, if $devicename is given, the Def-Hash for the SONOSPLAYER with the given name. +# +# Parameter $devicename = SONOSPLAYER devicename to be searched for, undef if searching for SONOS instead +# +######################################################################################## +sub SONOS_getDeviceDefHash($) { + my ($devicename) = @_; + + if (defined($devicename)) { + foreach my $fhem_dev (sort keys %main::defs) { + return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{NAME} eq $devicename); + } + } else { + foreach my $fhem_dev (sort keys %main::defs) { + return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOS'); + } + } + + SONOS_Log undef, 1, "The Method 'SONOS_getDeviceDefHash' cannot find the FHEM-Device according to '$devicename'. This should not happen!"; +} + +######################################################################################## +# +# SONOS_getSonosPlayerByUDN - Retrieves the Def-Hash for the SONOS-Device with the given UDN +# +######################################################################################## +sub SONOS_getSonosPlayerByUDN($) { + my ($udn) = @_; + + if (defined($udn)) { + foreach my $fhem_dev (sort keys %main::defs) { + return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{UDN} eq $udn); + } + } else { + foreach my $fhem_dev (sort keys %main::defs) { + return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOS'); + } + } + + SONOS_Log undef, 1, "The Method 'SONOS_getSonosPlayerByUDN' cannot find the FHEM-Device according to '$udn'. This should not happen!"; + + return undef; +} + +######################################################################################## +# +# SONOS_getSonosPlayerByRoomName - Retrieves the Def-Hash for the SONOS-Device with the given RoomName +# +######################################################################################## +sub SONOS_getSonosPlayerByRoomName($) { + my ($roomName) = @_; + + foreach my $fhem_dev (sort keys %main::defs) { + return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{READINGS}{roomName}{VAL} eq $roomName); + } + + SONOS_Log undef, 1, "The Method 'SONOS_getSonosPlayerByRoomName' cannot find the FHEM-Device according to '$roomName'. This should not happen!"; + + return undef; +} + +######################################################################################## +# +# SONOS_Undef - Implements UndefFn function +# +# Parameter hash = hash of the master, name +# +######################################################################################## +sub SONOS_Undef ($$) { + my ($hash, $name) = @_; + + RemoveInternalTimer($hash); + + DevIo_SimpleWrite($hash, "disconnect\n", 0); + DevIo_CloseDev($hash); + + return undef; +} + +######################################################################################## +# +# SONOS_Shutdown - Implements ShutdownFn function +# +# Parameter hash = hash of the master, name +# +######################################################################################## +sub SONOS_Shutdown ($$) { + my ($hash) = @_; + + RemoveInternalTimer($hash); + + # Wenn wir einen eigenen UPnP-Server gestartet haben, diesen hier auch wieder beenden, + # ansonsten nur die Verbindung kappen + if ($SONOS_StartedOwnUPnPServer) { + DevIo_SimpleWrite($hash, "shutdown\n", 0); + } else { + DevIo_SimpleWrite($hash, "disconnect\n", 0); + } + DevIo_CloseDev($hash); + + select(undef, undef, undef, 2); + + return undef; +} + +######################################################################################## +# +# SONOS_isInList - Checks, at which position the given value is in the given list +# Results in -1 if element not found +# +######################################################################################## +sub SONOS_posInList { + my($search, @list) = @_; + + for (my $i = 0; $i <= $#list; $i++) { + return $i if ($list[$i] && $search eq $list[$i]); + } + + return -1; +} + +######################################################################################## +# +# SONOS_isInList - Checks, if the given value is in the given list +# +######################################################################################## +sub SONOS_isInList { + my($search, @list) = @_; + + return 1 if SONOS_posInList($search, @list) >= 0; + return 0; +} + +######################################################################################## +# +# SONOS_Min - Retrieves the minimum of two values +# +######################################################################################## +sub SONOS_Min($$) { + $_[$_[0] > $_[1]] +} + +######################################################################################## +# +# SONOS_Max - Retrieves the maximum of two values +# +######################################################################################## +sub SONOS_Max($$) { + $_[$_[0] < $_[1]] +} + +######################################################################################## +# +# SONOS_GetRealPath - Retrieves the real (complete and absolute) path of the given file +# and converts all '\' to '/' +# +######################################################################################## +sub SONOS_GetRealPath($) { + my ($filename) = @_; + my $realFilename = realpath($filename); + + $realFilename =~ s/\\/\//g; + + return $realFilename +} + +######################################################################################## +# +# SONOS_GetAbsolutePath - Retreives the absolute path (without filename) +# +######################################################################################## +sub SONOS_GetAbsolutePath($) { + my ($filename) = @_; + my $absFilename = SONOS_GetRealPath($filename); + + return substr($absFilename, 0, rindex($absFilename, '/')); +} + +######################################################################################## +# +# SONOS_GetTimeFromString - Parse the given DateTime-String e.g. created by TimeNow(). +# +######################################################################################## +sub SONOS_GetTimeFromString($) { + my ($timeStr) = @_; + + return 0 if (!defined($timeStr)); + + eval { + use Time::Local; + if($timeStr =~ m/^(\d{4})-(\d{2})-(\d{2}) ([0-2]\d):([0-5]\d):([0-5]\d)$/) { + return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); + } + } +} + +######################################################################################## +# +# SONOS_GetTimeString - Gets the String for the given time +# +######################################################################################## +sub SONOS_GetTimeString($) { + my ($time) = @_; + + my @t = localtime($time); + + return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); +} + +######################################################################################## +# +# SONOS_TimeNow - Same as FHEM.PL-TimeNow. Neccessary due to forked process... +# +######################################################################################## +sub SONOS_TimeNow() { + return SONOS_GetTimeString(time()); +} + +######################################################################################## +# +# SONOS_Log - Log to the normal Log-command with additional Infomations like Thread-ID and the prefix 'SONOS' +# +######################################################################################## +sub SONOS_Log($$$) { + my ($udn, $level, $text) = @_; + + if (defined($SONOS_ListenPort)) { + if ($SONOS_Client_LogLevel >= $level) { + my ($seconds, $microseconds) = gettimeofday(); + + my @t = localtime($seconds); + my $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d", $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0]); + + if($SONOS_mseclog) { + $tim .= sprintf(".%03d", $microseconds / 1000); + } + + print "$tim $level: SONOS".threads->tid().": $text\n"; + } + } else { + my $hash = SONOS_getSonosPlayerByUDN($udn); + + eval { + Log3 $hash->{NAME}, $level, 'SONOS'.threads->tid().': '.$text; + }; + if ($@) { + Log $level, 'SONOS'.threads->tid().': '.$text; + } + } +} + +######################################################################################## +######################################################################################## +## +## Start of Telnet-Server-Part for Sonos UPnP-Messages +## +## If SONOS_ListenPort is defined, then we have to start a listening server +## +######################################################################################## +######################################################################################## +# Here starts the main-loop of the telnet-server +######################################################################################## +if (defined($SONOS_ListenPort)) { + $| = 1; + + my $runEndlessLoop = 1; + my $lastRenewSubscriptionCheckTime = time(); + + $SIG{'PIPE'} = 'IGNORE'; + $SIG{'CHLD'} = 'IGNORE'; + + $SIG{'INT'} = sub { + # Hauptschleife beenden + $SONOS_Client_NormalQueueWorking = 0; + $runEndlessLoop = 0; + + # Sub-Threads beenden, sofern vorhanden + if (($SONOS_Thread != -1) && defined(threads->object($SONOS_Thread))) { + threads->object($SONOS_Thread)->kill('INT')->detach(); + } + if (($SONOS_Thread_IsAlive != -1) && defined(threads->object($SONOS_Thread_IsAlive))) { + threads->object($SONOS_Thread_IsAlive)->kill('INT')->detach(); + } + if (($SONOS_Thread_PlayerRestore != -1) && defined(threads->object($SONOS_Thread_PlayerRestore))) { + threads->object($SONOS_Thread_PlayerRestore)->kill('INT')->detach(); + } + }; + + my $sock; + my $retryCounter = 10; + do { + eval { + socket($sock, AF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "Could not create socket: $!"; + bind($sock, sockaddr_in($SONOS_ListenPort, INADDR_ANY)) or die "Bind failed: $!"; + setsockopt($sock, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0)) or die "Setsockopt failed: $!"; + listen($sock, 10); + }; + if ($@) { + SONOS_Log undef, 0, "Can't bind Port $SONOS_ListenPort: $@"; + SONOS_Log undef, 0, 'Retries left (wait 30s): '.--$retryCounter; + + if (!$retryCounter) { + die 'Bind failed...'; + } + + select(undef, undef, undef, 30); + } + } while ($@); + SONOS_Log undef, 1, "$0 is listening to Port $SONOS_ListenPort"; + + # Accept incoming connections and talk to clients + $SONOS_Client_Selector = IO::Select->new($sock); + + while ($runEndlessLoop) { + # NormalQueueWorking wird für die Dauer einer Direkt-Wert-Anfrage deaktiviert, damit hier nicht blockiert und/oder zuviel weggelesen wird. + if ($SONOS_Client_NormalQueueWorking) { + # Das ganze blockiert eine kurze Zeit, um nicht 100% CPU-Last zu erzeugen + # Das bedeutet aber auch, dass Sende-Vorgänge um maximal den Timeout-Wert verzögert werden + my @ready = $SONOS_Client_Selector->can_read(0.1); + + # Falls wir hier auf eine Antwort reagieren würden, die gar nicht hierfür bestimmt ist, dann übergehen... + next if (!$SONOS_Client_NormalQueueWorking); + + # Nachschauen, ob Subscriptions erneuert werden müssen + if (time() - $lastRenewSubscriptionCheckTime > 1800) { + $lastRenewSubscriptionCheckTime = time (); + + foreach my $udn (@{$SONOS_Client_Data{PlayerUDNs}}) { + my %data; + $data{WorkType} = 'renewSubscription'; + $data{UDN} = $udn; + my @params = (); + $data{Params} = \@params; + + $SONOS_ComObjectTransportQueue->enqueue(\%data); + + # Signalhandler aufrufen, wenn er nicht sowieso noch läuft... + threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1); + } + } + + # Alle Bereit-Schreibenden verarbeiten + if ($SONOS_Client_SendQueue->pending() && !$SONOS_Client_SendQueue_Suspend) { + my @receiver = $SONOS_Client_Selector->can_write(0); + + # Prüfen, ob überhaupt ein Empfänger bereit ist. Sonst würden Befehle verloren gehen... + if (scalar(@receiver) > 0) { + while ($SONOS_Client_SendQueue->pending()) { + my $line = $SONOS_Client_SendQueue->dequeue(); + foreach my $so (@receiver) { + send($so, $line, 0); + } + } + } + } + + # Alle Bereit-Lesenden verarbeiten + foreach my $so (@ready) { + if ($so == $sock) { # New Connection read + my $client; + + my $addrinfo = accept($client, $sock); + setsockopt($client, SOL_SOCKET, SO_LINGER, pack("ii", 1, 0)); + my ($port, $iaddr) = sockaddr_in($addrinfo); + my $name = gethostbyaddr($iaddr, AF_INET); + + SONOS_Log undef, 1, "Connection accepted from $name:$port"; + + # Von dort kommt die Anfrage, dort finde ich den Telnet-Port von Fhem :-) + $SONOS_UseTelnetForQuestions_Host = $name; + + # Send Welcome-Message + send($client, "'This is UPnP-Server calling'\r\n", 0); + + $SONOS_Client_Selector->add($client); + } else { # Existing client calling + my $inp = <$so>; + + if (defined($inp)) { + # Abschließende Zeilenumbrüche abschnippeln + $inp =~ s/[\r\n]*$//; + + # Consume and send evt. reply + SONOS_Log undef, 5, "Received: '$inp'"; + SONOS_Client_ConsumeMessage($so, $inp); + } + } + } + } else { + # Wenn die Verarbeitung gerade unterbrochen sein soll, dann hier etwas warten, um keine 100% CPU-Last zu erzeugen + select(undef, undef, undef, 0.5); + } + } + + SONOS_Log undef, 0, 'Das Lauschen auf der Schnittstelle wurde beendet. Prozess endet nun auch...'; + + # Alle Handles entfernen und schliessen... + for my $cl ($SONOS_Client_Selector->handles()) { + $SONOS_Client_Selector->remove($cl); + shutdown($cl, 2); + close($cl); + } + + # Prozess beenden... + exit(0); +} + +# Wird für den FHEM-Modulpart benötigt +1; + +######################################################################################## +# SONOS_Client_Thread_Notifier: Notifies all clients with the given message +######################################################################################## +sub SONOS_Client_Notifier($) { + my ($msg) = @_; + $| = 1; + + state $setCurrentUDN; + + # Wenn hier ein SetCurrent ausgeführt werden soll, dann auch den lokalen Puffer aktualisieren + if ($msg =~ m/SetCurrent:(.*?):(.*)/) { + my $udnBuffer = ($setCurrentUDN eq 'undef') ? 'SONOS' : $setCurrentUDN; + $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$1} = $2; + } elsif ($msg =~ m/GetReadingsToCurrentHash:(.*?):(.*)/) { + $setCurrentUDN = $1; + } + + # Immer ein Zeilenumbruch anfügen... + $msg .= "\n" if (substr($msg, -1, 1) ne "\n"); + + $SONOS_Client_SendQueue->enqueue($msg); +} + +######################################################################################## +# SONOS_Client_SendReceive: Send and receive messages +######################################################################################## +sub SONOS_Client_SendReceive($) { + my ($msg) = @_; + + # Immer ein Zeilenumbruch anfügen... + $msg .= "\n" if (substr($msg, -1, 1) ne "\n"); + + my $answer; + $SONOS_Client_NormalQueueWorking = 0; + select(undef, undef, undef, 0.1); + + my @sender = $SONOS_Client_Selector->can_write(0); + foreach my $so (@sender) { + send($so, $msg, 0); + + do { + select(undef, undef, undef, 0.1); + recv($so, $answer, 30000, 0); + } while (!$answer); + } + + select(undef, undef, undef, 0.1); + $SONOS_Client_NormalQueueWorking = 1; + + return $answer; +} + +######################################################################################## +# SONOS_Client_SendReceiveTelnet: Send and receive messages +######################################################################################## +sub SONOS_Client_SendReceiveTelnet($) { + my ($msg) = @_; + + SONOS_Log undef, 4, "Telnet-Anfrage: $msg"; + + eval { + require Net::Telnet; + my $socket = Net::Telnet->new(Timeout => 30); + $socket->open(Host => $SONOS_UseTelnetForQuestions_Host, Port => $SONOS_UseTelnetForQuestions_Port); + $socket->telnetmode(0); + $socket->cmd(); + + my @lines = $socket->cmd('{ SONOS_AnswerQuery("'.$msg.'") }'); + my $answer = $lines[0]; + $answer =~ s/[\r\n]*$//; + + $socket->close(); + + return $answer; + }; + if ($@) { + SONOS_Log undef, 4, "Bei einer Telnet-Anfrage ist ein Fehler aufgetreten, es wird auf Normalanfrage umgestellt: $@"; + $SONOS_UseTelnetForQuestions = 0; + return $3 if ($msg =~ m/Q.:(.*?):(.*?):(.*)/); + } + + return "Error during processing: $msg"; +} + +######################################################################################## +# SONOS_Client_AskAttribute: Asks FHEM for a AttributeValue according to the given Attributename +######################################################################################## +sub SONOS_Client_AskAttribute($$$) { + my ($udn, $name, $default) = @_; + + my $val; + if ($SONOS_UseTelnetForQuestions) { + $val = SONOS_Client_SendReceiveTelnet('QA:'.$udn.':'.$name.':'.$default); + } else { + $val = SONOS_Client_SendReceive('QA:'.$udn.':'.$name.':'.$default); + } + $val =~ s/[\r\n]*$//; + $val = $1 if ($val =~ m/A:$udn:$name:(.*)/i); + + return $val; +} + +######################################################################################## +# SONOS_Client_AskReading: Asks FHEM for a ReadingValue according to the given Readingname +######################################################################################## +sub SONOS_Client_AskReading($$$) { + my ($udn, $name, $default) = @_; + + my $val; + if ($SONOS_UseTelnetForQuestions) { + $val = SONOS_Client_SendReceiveTelnet('QR:'.$udn.':'.$name.':'.$default); + } else { + $val = SONOS_Client_SendReceive('QR:'.$udn.':'.$name.':'.$default); + } + $val =~ s/[\r\n]*$//; + $val = $1 if ($val =~ m/R:$udn:$name:(.*)/i); + + return $val; +} + +######################################################################################## +# SONOS_Client_AskDefinition: Asks FHEM for a DefinitionValue according to the given name +######################################################################################## +sub SONOS_Client_AskDefinition($$$) { + my ($udn, $name, $default) = @_; + + my $val; + if ($SONOS_UseTelnetForQuestions) { + $val = SONOS_Client_SendReceiveTelnet('QD:'.$udn.':'.$name.':'.$default); + } else { + $val = SONOS_Client_SendReceive('QD:'.$udn.':'.$name.':'.$default); + } + $val =~ s/[\r\n]*$//; + $val = $1 if ($val =~ m/D:$udn:$name:(.*)/i); + + return $val; +} + +######################################################################################## +# SONOS_Client_Data_Retreive: Retrieves stored data, and calls AskXX if necessary +######################################################################################## +sub SONOS_Client_Data_Retreive($$$$) { + my ($udn, $reading, $name, $default) = @_; + + my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn; + + # Prüfen, ob die Anforderung überhaupt bedient werden darf + if ($reading eq 'attr') { + if (SONOS_posInList($name, @SONOS_PossibleAttributes) == -1) { + SONOS_Log undef, 0, "Ungültige Attribut-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!"; + exit(1); + } + } elsif ($reading eq 'def') { + if (SONOS_posInList($name, @SONOS_PossibleDefinitions) == -1) { + SONOS_Log undef, 0, "Ungültige Definitions-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!"; + exit(1); + } + } else { + if (SONOS_posInList($name, @SONOS_PossibleReadings) == -1) { + SONOS_Log undef, 0, "Ungültige Reading-Fhem-Informationsanforderung: $udnBuffer->$name.\nStoppe Prozess!"; + exit(1); + } + } + + # Anfrage zulässig, also ausliefern... + if (defined($SONOS_Client_Data{Buffer}->{$udnBuffer}) && defined($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name})) { + SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> ".$SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name}; + return $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name}; + } else { + SONOS_Log undef, 4, "SONOS_Client_Data_Retreive($udnBuffer, $reading, $name, $default) -> DEFAULT"; + return $default; + } + + ################################################## + # Alter Mechanismus mit Anfrage an Fhem... + #my $result = do { if (defined($SONOS_Client_Data{Buffer}->{$udnBuffer}) && defined($SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name})) { + # $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} + # } else { + # if ($reading eq 'attr') { + # SONOS_Client_AskAttribute($udn, $name, $default); + # } elsif ($reading eq 'def') { + # SONOS_Client_AskDefinition($udn, $name, $default); + # } else { + # SONOS_Client_AskReading($udn, $name, $default); + # } + # } + # }; + #$SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} = $result; + # + #return $result; +} + +######################################################################################## +# SONOS_Client_Data_Refresh: Send data and refreshs buffer +######################################################################################## +sub SONOS_Client_Data_Refresh($$$$) { + my ($sendCommand, $udn, $name, $value) = @_; + + my $udnBuffer = ($udn eq 'undef') ? 'SONOS' : $udn; + + $SONOS_Client_Data{Buffer}->{$udnBuffer}->{$name} = $value; + if ($sendCommand && ($sendCommand ne '')) { + SONOS_Client_Notifier($sendCommand.':'.$udn.':'.$name.':'.$value); + } +} + +######################################################################################## +# SONOS_Client_ConsumeMessage: Consumes the given message and give an evt. return +######################################################################################## +sub SONOS_Client_ConsumeMessage($$) { + my ($client, $msg) = @_; + + if (lc($msg) eq 'disconnect' || lc($msg) eq 'shutdown') { + SONOS_Log undef, 3, "Disconnecting client and shutdown server..." if (lc($msg) eq 'shutdown'); + SONOS_Log undef, 3, "Disconnecting client..." if (lc($msg) ne 'shutdown'); + + $SONOS_Client_Selector->remove($client); + + if ($SONOS_Thread != -1) { + my $thr = threads->object($SONOS_Thread); + + if ($thr) { + SONOS_Log undef, 3, 'Trying to kill Sonos_Thread...'; + $thr->kill('INT')->detach(); + } else { + SONOS_Log undef, 3, 'Sonos_Thread is already killed!'; + } + } + if ($SONOS_Thread_IsAlive != -1) { + my $thr = threads->object($SONOS_Thread_IsAlive); + + if ($thr) { + SONOS_Log undef, 3, 'Trying to kill IsAlive_Thread...'; + $thr->kill('INT')->detach(); + } else { + SONOS_Log undef, 3, 'IsAlive_Thread is already killed!'; + } + } + if ($SONOS_Thread_PlayerRestore != -1) { + my $thr = threads->object($SONOS_Thread_PlayerRestore); + + if ($thr) { + SONOS_Log undef, 3, 'Trying to kill PlayerRestore_Thread...'; + $thr->kill('INT')->detach(); + } else { + SONOS_Log undef, 3, 'PlayerRestore_Thread is already killed!'; + } + } + + shutdown($client, 2); + close($client); + + threads->self()->kill('INT') if (lc($msg) eq 'shutdown'); + } elsif (lc($msg) eq 'hello') { + send($client, "OK\r\n", 0); + } elsif (lc($msg) eq 'goaway') { + $SONOS_Client_Selector->remove($client); + shutdown($client, 2); + close($client); + } elsif ($msg =~ m/SetData:(.*?):(.*?):(.*?):(.*?):(.*)/i) { + $SONOS_Client_Data{SonosDeviceName} = $1; + $SONOS_Client_LogLevel = $2; + $SONOS_Client_Data{pingType} = $3; + + my @names = split(/,/, $4); + $SONOS_Client_Data{PlayerNames} = shared_clone(\@names); + + my @udns = split(/,/, $5); + $SONOS_Client_Data{PlayerUDNs} = shared_clone(\@udns); + + my @playeralive = (); + $SONOS_Client_Data{PlayerAlive} = shared_clone(\@playeralive); + + my %player = (); + $SONOS_Client_Data{Buffer} = shared_clone(\%player); + push @udns, 'SONOS'; + foreach my $elem (@udns) { + my %elemValues = (); + $SONOS_Client_Data{Buffer}->{$elem} = shared_clone(\%elemValues); + } + } elsif ($msg =~ m/SetValues:(.*?):(.*)/i) { + my $deviceName = $1; + my $deviceValues = $2; + my %elemValues = (); + + # Werte aus der Übergabe belegen + foreach my $elem (split(/\|/, $deviceValues)) { + if ($elem =~ m/(.*?)=(.*)/) { + $elemValues{$1} = uri_unescape($2); + } + } + + $SONOS_Client_Data{Buffer}->{$deviceName} = shared_clone(\%elemValues); + } elsif ($msg =~ m/DoWork:(.*?):(.*?):(.*)/i) { + my %data; + $data{WorkType} = $2; + $data{UDN} = $1; + + if (defined($3)) { + my @params = split(/,/, $3); + $data{Params} = \@params; + } else { + my @params = (); + $data{Params} = \@params; + } + + # Auf die Queue legen wenn Thread läuft und Signalhandler aufrufen, wenn er nicht sowieso noch läuft... + if ($SONOS_Thread != -1) { + $SONOS_ComObjectTransportQueue->enqueue(\%data); + threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1); + } + } elsif (lc($msg) eq 'startthread') { + # Discover-Thread + $SONOS_Thread = threads->create(\&SONOS_Discover)->tid(); + + # IsAlive-Checker-Thread + if (lc($SONOS_Client_Data{pingType}) ne 'none') { + $SONOS_Thread_IsAlive = threads->create(\&SONOS_Client_IsAlive)->tid(); + } + + # Playerrestore-Thread + $SONOS_Thread_PlayerRestore = threads->create(\&SONOS_RestoreOldPlaystate)->tid(); + } else { + SONOS_Log undef, 2, "ConsumMessage: Sorry. I don't understand you - '$msg'."; + send($client, "Sorry. I don't understand you - '$msg'.\r\n", 0); + } +} + +######################################################################################## +# SONOS_Client_IsAlive: Checks of the clients are already available +######################################################################################## +sub SONOS_Client_IsAlive() { + my $interval = SONOS_Max(10, SONOS_Client_Data_Retreive('undef', 'def', 'INTERVAL', 0)); + my $stepInterval = 0.5; + + SONOS_Log undef, 1, 'IsAlive-Thread gestartet. Warte 120 Sekunden und pruefe dann alle '.$interval.' Sekunden...'; + + my $runEndlessLoop = 1; + + $SIG{'PIPE'} = 'IGNORE'; + $SIG{'CHLD'} = 'IGNORE'; + + $SIG{'INT'} = sub { + $runEndlessLoop = 0; + }; + + # Erst nach einer Weile wartens anfangen zu arbeiten. Bis dahin sollten alle Player im Netz erkannt, und deren Konfigurationen bekannt sein. + my $counter = 0; + do { + select(undef, undef, undef, 0.5); + } while (($counter++ < 240) && $runEndlessLoop); + + my $stepCounter = 0; + while($runEndlessLoop) { + select(undef, undef, undef, $stepInterval); + + next if (($stepCounter += $stepInterval) < $interval); + $stepCounter = 0; + + # Alle bekannten Player durchgehen, wenn der Thread nicht beendet werden soll + if ($runEndlessLoop) { + my @list = @{$SONOS_Client_Data{PlayerAlive}}; + my @toAnnounce = (); + for(my $i = 0; $i <= $#list; $i++) { + next if (!$list[$i]); + + if (!SONOS_IsAlive($list[$i])) { + # Auf die Entfernen-Meldeliste setzen + push @toAnnounce, $list[$i]; + + # Wenn er nicht mehr am Leben ist, dann auch aus der Aktiven-Liste entfernen + delete @{$SONOS_Client_Data{PlayerAlive}}[$i]; + } + } + + # Wenn ein Player gerade verschwunden ist, dann dem (verbleibenden) Sonos-System das mitteilen + foreach my $toDeleteElem (@toAnnounce) { + if ($toDeleteElem =~ m/(^.*)_/) { + $toDeleteElem = $1; + SONOS_Log undef, 3, 'ReportUnresponsiveDevice: '.$toDeleteElem; + foreach my $udn (@{$SONOS_Client_Data{PlayerAlive}}) { + my %data; + $data{WorkType} = 'reportUnresponsiveDevice'; + $data{UDN} = $udn; + my @params = (); + push @params, $toDeleteElem; + $data{Params} = \@params; + + $SONOS_ComObjectTransportQueue->enqueue(\%data); + + # Signalhandler aufrufen, wenn er nicht sowieso noch läuft... + threads->object($SONOS_Thread)->kill('HUP') if ($SONOS_ComObjectTransportQueue->pending() == 1); + + # Da ich das nur an den ersten verfügbaren Player senden muss, kann hier die Schleife direkt beendet werden + last; + } + } + } + } + } + + SONOS_Log undef, 1, 'IsAlive-Thread wurde beendet.'; + $SONOS_Thread_IsAlive = -1; +} +######################################################################################## +######################################################################################## +## +## End of Telnet-Server-Part for Sonos UPnP-Messages +## +######################################################################################## +######################################################################################## + + +=pod +=begin html + +<a name="SONOS"></a> +<h3>SONOS</h3> +<p>FHEM-Module to communicate with the Sonos-System via UPnP</p> +<p>For more informations have also a closer look at the wiki at <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a></p> +<p>For correct functioning of this module it is neccessary to have some Perl-Modules installed, which has eventually installed manually:<ul> +<li><code>LWP::Simple</code></li> +<li><code>LWP::UserAgent</code></li> +<li><code>SOAP::Lite</code></li> +<li><code>HTTP::Request</code></li></ul></p> +<p><b>Attention!</b><br />This Module will not be functioning on any platform, because of the use of Threads and the neccessary Perl-modules.</p> +<p>More information is given in a (german) Wiki-article: <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a></p> +<p>The system conists of two different components:<br /> +1. A UPnP-Client which runs as a standalone process in the background and takes the communications to the sonos-components.<br /> +2. The FHEM-module itself which connects to the UPnP-client to make fhem able to work with sonos.<br /><br /> +The client will be startet by the module itself if not done in another way.<br /> +You can start this client on your own (to let it run instantly and independent from FHEM):<br /> +<code>perl 00_SONOS.pm 4711</code>: Starts a UPnP-Client in an independant way who listens to connections on port 4711. This process can run a long time, FHEM can connect and disconnect to it.</p> +<h4>Example</h4> +<p> +<code>define Sonos SONOS localhost:4711 30</code> +</p> +<br /> +<a name="SONOSdefine"></a> +<h4>Define</h4> +<code>define <name> SONOS [upnplistener] [[[interval] waittime] delaytime]</code> + <br /><br /> Define a Sonos interface to communicate with a Sonos-System.<br /> +<p> +<code>[upnplistener]</code><br />The name and port of the external upnp-listener. If not given, defaults to <code>localhost:4711</code>. The port has to be a free portnumber on your system. If you don't start a server on your own, the script does itself.<br />If you start it yourself write down the correct informations to connect.</p> +<p> +<code>[interval]</code><br /> The interval is for alive-checking of Zoneplayer-device, because no message come if the host disappear :-)<br />If omitted a value of 10 seconds is the default.</p> +<p> +<code>[waittime]</code><br /> With this value you can configure the waiting time for the starting of the Subprocess.</p> +<p> +<code>[delaytime]</code><br /> With this value you can configure a delay time before starting the network-part.</p> +<br /> +<br /> +<a name="SONOSset"></a> +<h4>Set</h4> +<ul> +<li><b>Control-Commands</b><ul> +<li><a name="SONOS_setter_PauseAll"> +<code>set <name> PauseAll</code></a> +<br />Pause all Zoneplayer.</li> +<li><a name="SONOS_setter_StopAll"> +<code>set <name> StopAll</code></a> +<br />Stops all Zoneplayer.</li> +</ul></li> +<li><b>Group-Commands</b><ul> +<li><a name="SONOS_setter_Groups"> +<code>set <name> Groups <GroupDefinition></code></a> +<br />Sets the current groups on the whole Sonos-System. The format is the same as retreived by getter 'Groups'.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSget"></a> +<h4>Get</h4> +<ul> +<li><b>Group-Commands</b><ul> +<li><a name="SONOS_getter_Groups"> +<code>get <name> Groups</code></a> +<br />Retreives the current group-configuration of the Sonos-System. The format is a comma-separated List of Lists with devicenames e.g. <code>[Sonos_Kueche], [Sonos_Wohnzimmer, Sonos_Schlafzimmer]</code>. In this example there are two groups: the first consists of one player and the second consists of two players.<br /> +The order in the sublists are important, because the first entry defines the so-called group-coordinator (in this case <code>Sonos_Wohnzimmer</code>), from which the current playlist and the current title playing transferred to the other member(s).</li> +</ul></li> +</ul> +<br /> +<a name="SONOSattr"></a> +<h4>Attributes</h4> +<ul> +<li><b>Common</b><ul> +<li><a name="SONOS_attribut_characterDecoding"><code>attr <name> characterDecoding <codingname></code> +</a><br />With this attribute you can define a character-decoding-class. E.g. <UTF-8>. Default is <CP-1252>.</li> +<li><a name="SONOS_attribut_pingType"><code>attr <name> pingType <string></code> +</a><br /> One of (none,tcp,udp,icmp,syn). Defines which pingType for alive-Checking has to be used. If set to 'none' no checks will be done.</li> +</ul></li> +<li><b>Proxy Configuration</b><ul> +<li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><code>attr <name> generateProxyAlbumArtURLs <int></code> +</a><br />One of (0, 1). If defined, all Cover-Links (the readings "currentAlbumArtURL" and "nextAlbumArtURL") are generated as links to the internal Sonos-Module-Proxy. It can be useful if you access Fhem over an external proxy and therefore have no access to the local network (the URLs are direct URLs to the Sonosplayer instead).</li> +<li><a name="SONOS_attribut_proxyCacheDir"><code>attr <name> proxyCacheDir <Path></code> +</a><br />Defines a directory where the cached Coverfiles can be placed. If not defined "/tmp" will be used.</li> +<li><a name="SONOS_attribut_proxyCacheTime"><code>attr <name> proxyCacheTime <int></code> +</a><br />A time in seconds. With a definition other than "0" the caching mechanism of the internal Sonos-Module-Proxy will be activated. If the filetime of the chached cover is older than this time, it will be reloaded from the Sonosplayer.</li> +</ul></li> +<li><b>Speak Configuration</b><ul> +<li><a name="SONOS_attribut_targetSpeakDir"><code>attr <name> targetSpeakDir <string></code> +</a><br /> Defines, which Directory has to be used for the Speakfiles</li> +<li><a name="SONOS_attribut_targetSpeakURL"><code>attr <name> targetSpeakURL <string></code> +</a><br /> Defines, which URL has to be used for accessing former stored Speakfiles as seen from the SonosPlayer</li> +<li><a name="SONOS_attribut_targetSpeakFileTimestamp"><code>attr <name> targetSpeakFileTimestamp <int></code> +</a><br /> One of (0, 1). Defines, if the Speakfile should have a timestamp in his name. That makes it possible to store all historical Speakfiles.</li> +<li><a name="SONOS_attribut_targetSpeakFileHashCache"><code>attr <name> targetSpeakFileHashCache <int></code> +</a><br /> One of (0, 1). Defines, if the Speakfile should have a hash-value in his name. If this value is set to one an already generated file with the same hash is re-used and not newly generated.</li> +<li><a name="SONOS_attribut_Speak1"><code>attr <name> Speak1 <Fileextension>:<Commandline></code> +</a><br />Defines a systemcall commandline for generating a speaking file out of the given text. If such an attribute is defined, an associated setter at the Sonosplayer-Device is available. The following placeholders are available:<br />'''%language%''': Will be replaced by the given language-parameter<br />'''%filename%''': Will be replaced by the complete target-filename (incl. fileextension).<br />'''%text%''': Will be replaced with the given text</li> +<li><a name="SONOS_attribut_Speak2"><code>attr <name> Speak2 <Fileextension>:<Commandline></code> +</a><br />See Speak1</li> +<li><a name="SONOS_attribut_Speak3"><code>attr <name> Speak3 <Fileextension>:<Commandline></code> +</a><br />See Speak1</li> +<li><a name="SONOS_attribut_Speak4"><code>attr <name> Speak4 <Fileextension>:<Commandline></code> +</a><br />See Speak1</li> +<li><a name="SONOS_attribut_SpeakCover"><code>attr <name> SpeakCover <Filename></code> +</a><br />Defines a Cover for use by the speak generation process. If not defined the Fhem-logo will be used.</li> +<li><a name="SONOS_attribut_Speak1Cover"><code>attr <name> Speak1Cover <Filename></code> +</a><br />See SpeakCover</li> +<li><a name="SONOS_attribut_Speak2Cover"><code>attr <name> Speak2Cover <Filename></code> +</a><br />See SpeakCover</li> +<li><a name="SONOS_attribut_Speak3Cover"><code>attr <name> Speak3Cover <Filename></code> +</a><br />See SpeakCover</li> +<li><a name="SONOS_attribut_Speak4Cover"><code>attr <name> Speak4Cover <Filename></code> +</a><br />See SpeakCover</li> +</ul></li> +</ul> + +=end html + +=begin html_DE + +<a name="SONOS"></a> +<h3>SONOS</h3> +<p>FHEM-Modul für die Anbindung des Sonos-Systems via UPnP</p> +<p>Für weitere Hinweise und Beschreibungen bitte auch im Wiki unter <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a> nachschauen.</p> +<p>Für die Verwendung sind Perlmodule notwendig, die unter Umständen noch nachinstalliert werden müssen:<ul> +<li><code>LWP::Simple</code></li> +<li><code>LWP::UserAgent</code></li> +<li><code>SOAP::Lite</code></li> +<li><code>HTTP::Request</code></li></ul></p> +<p><b>Achtung!</b><br />Das Modul wird nicht auf jeder Plattform lauffähig sein, da Threads und die angegebenen Perl-Module verwendet werden.</p> +<p>Mehr Informationen im (deutschen) Wiki-Artikel: <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a></p> +<p>Das System besteht aus zwei Komponenten:<br /> +1. Einem UPnP-Client, der als eigener Prozess im Hintergrund ständig läuft, und die Kommunikation mit den Sonos-Geräten übernimmt.<br /> +2. Dem eigentlichen FHEM-Modul, welches mit dem UPnP-Client zusammenarbeitet, um die Funktionalität in FHEM zu ermöglichen.<br /><br /> +Der Client wird im Notfall automatisch von Modul selbst gestartet.<br /> +Man kann den Server unabhängig von FHEM selbst starten (um ihn dauerhaft und unabhängig von FHEM laufen zu lassen):<br /> +<code>perl 00_SONOS.pm 4711</code>: Startet einen unabhängigen Server, der auf Port 4711 auf eingehende FHEM-Verbindungen lauscht. Dieser Prozess kann dauerhaft laufen, FHEM kann sich verbinden und auch wieder trennen.</p> +<h4>Beispiel</h4> +<p> +<code>define Sonos SONOS localhost:4711 30</code> +</p> +<br /> +<a name="SONOSdefine"></a> +<h4>Definition</h4> +<code>define <name> SONOS [upnplistener] [[[interval] waittime] delaytime]</code> + <br /><br /> Definiert das Sonos interface für die Kommunikation mit dem Sonos-System.<br /> +<p> +<code>[upnplistener]</code><br />Name und Port eines externen UPnP-Client. Wenn nicht angegebenen wird <code>localhost:4711</code> festgelegt. Der Port muss eine freie Portnummer ihres Systems sein. <br />Wenn sie keinen externen Client gestartet haben, startet das Skript einen eigenen.<br />Wenn sie einen eigenen Dienst gestartet haben, dann geben sie hier die entsprechenden Informationen an.</p> +<p> +<code>[interval]</code><br /> Das Interval wird für die Überprüfung eines Zoneplayers benötigt. In diesem Interval wird nachgeschaut, ob der Player noch erreichbar ist, da sich ein Player nicht mehr abmeldet, wenn er abgeschaltet wird :-)<br />Wenn nicht angegeben, wird ein Wert von 10 Sekunden angenommen.</p> +<p> +<code>[waittime]</code><br /> Hiermit wird die Wartezeit eingestellt, die nach dem Starten des SubProzesses darauf gewartet wird.</p> +<p> +<code>[delaytime]</code><br /> Hiermit kann eine Verzögerung eingestellt werden, die vor dem Starten des Netzwerks gewartet wird.</p> +<br /> +<br /> +<a name="SONOSset"></a> +<h4>Set</h4> +<ul> +<li><b>Steuerbefehle</b><ul> +<li><a name="SONOS_setter_PauseAll"> +<code>set <name> PauseAll</code></a> +<br />Pausiert die Wiedergabe in allen Zonen.</li> +<li><a name="SONOS_setter_StopAll"> +<code>set <name> StopAll</code></a> +<br />Stoppt die Wiedergabe in allen Zonen.</li> +</ul></li> +<li><b>Gruppenbefehle</b><ul> +<li><a name="SONOS_setter_Groups"> +<code>set <name> Groups <GroupDefinition></code></a> +<br />Setzt die aktuelle Gruppierungskonfiguration der Sonos-Systemlandschaft. Das Format ist jenes, welches auch von dem Get-Befehl 'Groups' geliefert wird.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSget"></a> +<h4>Get</h4> +<ul> +<li><b>Gruppenbefehle</b><ul> +<li><a name="SONOS_getter_Groups"> +<code>get <name> Groups</code></a> +<br />Liefert die aktuelle Gruppierungskonfiguration der Sonos Systemlandschaft zurück. Das Format ist eine Kommagetrennte Liste von Listen mit Devicenamen, also z.B. <code>[Sonos_Kueche], [Sonos_Wohnzimmer, Sonos_Schlafzimmer]</code>. In diesem Beispiel sind also zwei Gruppen definiert, von denen die erste aus einem Player und die zweite aus Zwei Playern besteht.<br /> +Dabei ist die Reihenfolge innerhalb der Unterlisten wichtig, da der erste Eintrag der sogenannte Gruppenkoordinator ist (in diesem Fall also <code>Sonos_Wohnzimmer</code>), von dem die aktuelle Abspielliste un der aktuelle Titel auf die anderen Gruppenmitglieder übernommen wird.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSattr"></a> +<h4>Attribute</h4> +<ul> +<li><b>Grundsätzliches</b><ul> +<li><a name="SONOS_attribut_characterDecoding"><code>attr <name> characterDecoding <codingname></code> +</a><br />Hiermit kann die Zeichendekodierung eingestellt werden. Z.b. <UTF-8>. Standardmäßig wird <CP-1252> verwendet.</li> +<li><a name="SONOS_attribut_pingType"><code>attr <name> pingType <string></code> +</a><br /> One of (none,tcp,udp,icmp,syn). Gibt an, welche Methode für die Ping-Überprüfung verwendet werden soll. Wenn 'none' angegeben wird, dann wird keine Überprüfung gestartet.</li> +</ul></li> +<li><b>Proxy-Einstellungen</b><ul> +<li><a name="SONOS_attribut_generateProxyAlbumArtURLs"><code>attr <name> generateProxyAlbumArtURLs <int></code> +</a><br /> Aus (0, 1). Wenn aktiviert, werden alle Cober-Links als Proxy-Aufrufe an Fhem generiert. Dieser Proxy-Server wird vom Sonos-Modul bereitgestellt. In der Grundeinstellung erfolgt kein Caching der Cover, sondern nur eine Durchreichung der Cover von den Sonosplayern (Damit ist der Zugriff durch einen externen Proxyserver auf Fhem möglich).</li> +<li><a name="SONOS_attribut_proxyCacheDir"><code>attr <name> proxyCacheDir <Path></code> +</a><br /> Hiermit wird das Verzeichnis festgelegt, in dem die Cober zwischengespeichert werden. Wenn nicht festegelegt, so wird "/tmp" verwendet.</li> +<li><a name="SONOS_attribut_proxyCacheTime"><code>attr <name> proxyCacheTime <int></code> +</a><br /> Mit einer Angabe ungleich 0 wird der Caching-Mechanismus des Sonos-Modul-Proxy-Servers aktiviert. Dabei werden Cover, die im Cache älter sind als diese Zeitangabe in Sekunden, neu vom Sonosplayer geladen, alle anderen direkt ausgeliefert, ohne den Player zu fragen.</li> +</ul></li> +<li><b>Sprachoptionen</b><ul> +<li><a name="SONOS_attribut_targetSpeakDir"><code>attr <name> targetSpeakDir <string></code> +</a><br /> Gibt an, welches Verzeichnis für die Ablage des MP3-Files der Textausgabe verwendet werden soll</li> +<li><a name="SONOS_attribut_targetSpeakURL"><code>attr <name> targetSpeakURL <string></code> +</a><br /> Gibt an, unter welcher Adresse der ZonePlayer das unter targetSpeakDir angegebene Verzeichnis erreichen kann.</li> +<li><a name="SONOS_attribut_targetSpeakFileTimestamp"><code>attr <name> targetSpeakFileTimestamp <int></code> +</a><br /> One of (0, 1). Gibt an, ob die erzeugte MP3-Sprachausgabedatei einen Zeitstempel erhalten soll (1) oder nicht (0).</li> +<li><a name="SONOS_attribut_targetSpeakFileHashCache"><code>attr <name> targetSpeakFileHashCache <int></code> +</a><br /> One of (0, 1). Gibt an, ob die erzeugte Sprachausgabedatei einen Hashwert erhalten soll (1) oder nicht (0). Wenn dieser Wert gesetzt wird, dann wird eine bereits bestehende Datei wiederverwendet, und nicht neu erzeugt.</li> +<li><a name="SONOS_attribut_Speak1"><code>attr <name> Speak1 <Fileextension>:<Commandline></code> +</a><br />Hiermit kann ein Systemaufruf definiert werden, der zu Erzeugung einer Sprachausgabe verwendet werden kann. Sobald dieses Attribut definiert wurde, ist ein entsprechender Setter am Sonosplayer verfügbar.<br />Es dürfen folgende Platzhalter verwendet werden:<br />'''%language%''': Wird durch die eingegebene Sprache ersetzt<br />'''%filename%''': Wird durch den kompletten Dateinamen (inkl. Dateiendung) ersetzt.<br />'''%text%''': Wird durch den zu übersetzenden Text ersetzt.</li> +<li><a name="SONOS_attribut_Speak2"><code>attr <name> Speak2 <Fileextension>:<Commandline></code> +</a><br />Siehe Speak1</li> +<li><a name="SONOS_attribut_Speak3"><code>attr <name> Speak3 <Fileextension>:<Commandline></code> +</a><br />Siehe Speak1</li> +<li><a name="SONOS_attribut_Speak4"><code>attr <name> Speak4 <Fileextension>:<Commandline></code> +</a><br />Siehe Speak1</li> +<li><a name="SONOS_attribut_SpeakCover"><code>attr <name> SpeakCover <Absolute-Imagepath></code> +</a><br />Hiermit kann ein JPG- oder PNG-Bild als Cover für die Sprachdurchsagen definiert werden.</li> +<li><a name="SONOS_attribut_Speak1Cover"><code>attr <name> Speak1Cover <Absolute-Imagepath></code> +</a><br />Analog zu SpeakCover für Speak1.</li> +<li><a name="SONOS_attribut_Speak2Cover"><code>attr <name> Speak2Cover <Absolute-Imagepath></code> +</a><br />Analog zu SpeakCover für Speak2.</li> +<li><a name="SONOS_attribut_Speak3Cover"><code>attr <name> Speak3Cover <Absolute-Imagepath></code> +</a><br />Analog zu SpeakCover für Speak3.</li> +<li><a name="SONOS_attribut_Speak3Cover"><code>attr <name> Speak3Cover <Absolute-Imagepath></code> +</a><br />Analog zu SpeakCover für Speak3.</li> +<li><a name="SONOS_attribut_Speak4Cover"><code>attr <name> Speak4Cover <Absolute-Imagepath></code> +</a><br />Analog zu SpeakCover für Speak4.</li> +</ul></li> +</ul> + +=end html_DE +=cut \ No newline at end of file diff --git a/fhem/FHEM/21_SONOSPLAYER.pm b/fhem/FHEM/21_SONOSPLAYER.pm new file mode 100755 index 000000000..4635ac294 --- /dev/null +++ b/fhem/FHEM/21_SONOSPLAYER.pm @@ -0,0 +1,1286 @@ +######################################################################################## +# +# SONOSPLAYER.pm (c) by Reiner Leins, 2014 +# rleins at lmsoft dot de +# +# $Id$ +# +# FHEM module to work with Sonos-Zoneplayers +# +# Internal Version 2.6 - December, 2014 +# +# define <name> SONOSPLAYER <UDN> +# +# where <name> may be replaced by any name string +# <udn> is the Zoneplayer Identification +# +######################################################################################## +# Changelog +# +# ab 2.2 Changelog nur noch in der Datei 00_SONOS +# +# 2.1: Neuen Befehl 'CurrentPlaylist' eingeführt +# +# 2.0: Neue Konzeptbasis eingebaut +# Man kann Gruppen auf- und wieder abbauen +# PlayURI kann nun einen Devicenamen entgegennehmen, und spielt dann den AV-Eingang des angegebenen Raumes ab +# Alle Steuerbefehle werden automatisch an den jeweiligen Gruppenkoordinator gesendet, sodass die Abspielanweisungen immer korrekt durchgeführt werden +# Es gibt neue Lautstärke- und Mute-Einstellungen für Gruppen ingesamt +# +# 1.12: TrackURI hinzugefügt +# Alarmbenutzung hinzugefügt +# Schlummerzeit hinzugefügt (Reading SleepTimer) +# DailyIndexRefreshTime hinzugefügt +# +# 1.11: Shuffle, Repeat und CrossfadeMode können nun gesetzt und abgefragt werden +# +# 1.10: LastAction-Readings werden nun nach eigener Konvention am Anfang groß geschrieben. Damit werden 'interne Variablen' von den Informations-Readings durch Groß/Kleinschreibung unterschieden +# Volume, Balance und HeadphonConnected können nun auch in InfoSummarize und StateVariable verwendet werden. Damit sind dies momentan die einzigen 'interne Variablen', die dort verwendet werden können +# Attribut 'generateVolumeEvent' eingeführt. +# Getter und Setter 'Balance' eingeführt. +# Reading 'HeadphoneConnected' eingeführt. +# Reading 'Mute' eingeführt. +# InfoSummarize-Features erweitert: 'instead' und 'emptyval' hinzugefügt +# +# 1.9: +# +# 1.8: minVolume und maxVolume eingeführt. Damit kann nun der Lautstärkeregelbereich der ZonePlayer festgelegt werden +# +# 1.7: Fehlermeldung bei aktivem TempPlaying und damit Abbruch der Anforderung deutlicher geschrieben +# +# 1.6: Speak hinzugefügt +# +# Versionsnummer zu 00_SONOS angeglichen +# +# 1.3: Zusätzliche Befehle hinzugefügt +# +# 1.2: Einrückungen im Code korrigiert +# +# 1.1: generateInfoAnswerOnSet eingeführt (siehe Doku im Wiki) +# generateVolumeSlider eingeführt (siehe Doku im Wiki) +# +# 1.0: Initial Release +# +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## +# Uses Declarations +######################################################################################## +package main; + +use vars qw{%attr %defs}; +use strict; +use warnings; +use URI::Escape; +use Thread::Queue; + +require 'HttpUtils.pm'; + +sub Log($$); +sub Log3($$$); +sub SONOSPLAYER_Log($$$); + +######################################################################################## +# Variable Definitions +######################################################################################## +my %gets = ( + 'CurrentTrackPosition' => '', + 'Playlists' => '', + 'PlaylistsWithCovers' => '', + 'Favourites' => '', + 'FavouritesWithCovers' => '', + 'Radios' => '', + 'RadiosWithCovers' => '', + 'Alarm' => 'ID', + 'EthernetPortStatus' => 'PortNum', + 'PossibleRoomIcons' => '' +); + +my %sets = ( + 'Play' => '', + 'Pause' => '', + 'Stop' => '', + 'Next' => '', + 'Previous' => '', + 'LoadPlaylist' => 'playlistname', + 'StartPlaylist' => 'playlistname', + 'SavePlaylist' => 'playlistname', + 'CurrentPlaylist' => '', + 'EmptyPlaylist' => '', + 'StartFavourite' => 'favouritename', + 'CreateThemeList' => 'searchField=searchValue', + 'LoadRadio' => 'radioname', + 'StartRadio' => 'radioname', + 'PlayURI' => 'songURI', + 'PlayURITemp' => 'songURI', + 'AddURIToQueue' => 'songURI', + 'Speak' => 'volume language text', + 'Mute' => 'state', + 'Shuffle' => 'state', + 'Repeat' => 'state', + 'CrossfadeMode' => 'state', + 'LEDState' => 'state', + 'MuteT' => '', + 'VolumeD' => '', + 'VolumeU' => '', + 'Volume' => 'volumelevel', + 'VolumeSave' => 'volumelevel', + 'VolumeRestore' => '', + 'Balance' => 'balancevalue', + 'Loudness' => 'state', + 'Bass' => 'basslevel', + 'Treble' => 'treblelevel', + 'CurrentTrackPosition' => 'timeposition', + 'Track' => 'tracknumber|Random', + 'Alarm' => 'create|update|delete ID valueHash', + 'DailyIndexRefreshTime' => 'timestamp', + 'SleepTimer' => 'time', + 'AddMember' => 'member_devicename', + 'RemoveMember' => 'member_devicename', + 'GroupVolume' => 'volumelevel', + 'SnapshotGroupVolume' => '', + 'GroupMute' => 'state', + 'Reboot' => '', + 'Wifi' => 'state', + 'Name' => 'roomName', + 'Icon' => 'iconName' +); + +my @possibleRoomIcons = qw(bathroom library office foyer dining tvroom hallway garage garden guestroom den bedroom kitchen portable media family pool masterbedroom playroom patio living); + +######################################################################################## +# +# SONOSPLAYER_Initialize +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOSPLAYER_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "SONOSPLAYER_Define"; + $hash->{UndefFn} = "SONOSPLAYER_Undef"; + $hash->{GetFn} = "SONOSPLAYER_Get"; + $hash->{SetFn} = "SONOSPLAYER_Set"; + $hash->{StateFn} = "SONOSPLAYER_State"; + $hash->{NotifyFn} = 'SONOSPLAYER_Notify'; + + $hash->{AttrList}= "disable:0,1 generateVolumeSlider:0,1 generateVolumeEvent:0,1 generateSomethingChangedEvent:0,1 generateInfoSummarize1 generateInfoSummarize2 generateInfoSummarize3 generateInfoSummarize4 stateVariable:TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackURI,nextAlbumArtURI,nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,Shuffle,Repeat,CrossfadeMode,Balance,HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,InfoSummarize2,InfoSummarize3,InfoSummarize4 model minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone VolumeStep getAlarms:0,1 buttonEvents ".$readingFnAttributes; + + return undef; +} + +######################################################################################## +# +# SONOSPLAYER_Define - Implements DefFn function +# +# Parameter hash = hash of device addressed, def = definition string +# +######################################################################################## +sub SONOSPLAYER_Define ($$) { + my ($hash, $def) = @_; + + # define <name> SONOSPLAYER <udn> + # e.g.: define Sonos_Wohnzimmer SONOSPLAYER RINCON_000EFEFEFEF401400 + my @a = split("[ \t]+", $def); + + my ($name, $udn); + + # default + $name = $a[0]; + $udn = $a[2]; + + # check syntax + return "SONOSPLAYER: Wrong syntax, must be define <name> SONOSPLAYER <udn>" if(int(@a) < 3); + + readingsSingleUpdate($hash, "state", 'init', 1); + readingsSingleUpdate($hash, "presence", 'disappeared', 0); # Grund-Initialisierung, falls der Player sich nicht zurückmelden sollte... + + $hash->{UDN} = $udn; + readingsSingleUpdate($hash, "state", 'initialized', 1); + + return undef; +} + +####################################################################################### +# +# SONOSPLAYER_State - StateFn, used for deleting unused or initialized Readings... +# +######################################################################################## +sub SONOSPLAYER_State($$$$) { + my ($hash, $time, $name, $value) = @_; + + # Die folgenden Readings müssen immer neu initialisiert verwendet werden, und dürfen nicht aus dem Statefile verwendet werden + #return 'Reading '.$hash->{NAME}."->$name must not be used out of statefile. This is not an error! This happens due to restrictions of Fhem." if ($name eq 'presence') || ($name eq 'LastActionResult') || ($name eq 'AlarmList') || ($name eq 'AlarmListIDs') || ($name eq 'AlarmListVersion'); + if (($name eq 'presence') || ($name eq 'LastActionResult') || ($name eq 'AlarmList') || ($name eq 'AlarmListIDs') || ($name eq 'AlarmListVersion')) { + SONOSPLAYER_Log undef, 4, 'StateFn-Call. Ignore the following Reading: '.$hash->{NAME}.'->'.$name.'('.(defined($value) ? $value : '').')'; + + setReadingsVal($hash, $name, '~~NotLoadedMarker~~', TimeNow()); + } + + # Die folgenden Readings werden nicht mehr benötigt, und werden hiermit entfernt... + return 'Reading '.$hash->{NAME}."->$name is now unused and is ignored for the future for all Zoneplayer-Types." if ($name eq 'LastGetActionName') || ($name eq 'LastGetActionResult') || ($name eq 'LastSetActionName') || ($name eq 'LastSetActionResult') || ($name eq 'LastSubscriptionsRenew') || ($name eq 'LastSubscriptionsResult') || ($name eq 'SetMakeStandaloneGroup') || ($name eq 'CurrentTempPlaying') || ($name eq 'SetWRONG'); + + return undef; +} + +######################################################################################## +# +# SONOSPLAYER_Notify - Implements NotifyFn function +# +######################################################################################## +sub SONOSPLAYER_Notify() { + my ($hash, $notifyhash) = @_; + + return undef; + + # Das folgende habe ich erstmal wieder entfernt, da man ja öfter im laufenden Betrieb die Einstellungen speichert, und den Sonos-Komponenten dann immer wichtige Informationen für den Betrieb fehlen (nicht jedes Save wird vor dem Neustart von Fhem ausgeführt) + #if (($notifyhash->{NAME} eq 'global') && (($notifyhash->{CHANGED}[0] eq 'SAVE') || ($notifyhash->{CHANGED}[0] eq 'SHUTDOWN'))) { + # SONOSPLAYER_Log undef, 3, $hash->{NAME}.' has detected a global:'.$notifyhash->{CHANGED}[0].'-Event. Clear out some readings before...'; + # + # # Einige Readings niemals speichern + # delete($defs{$hash->{NAME}}{READINGS}{presence}); + # delete($defs{$hash->{NAME}}{READINGS}{LastActionResult}); + # delete($defs{$hash->{NAME}}{READINGS}{AlarmList}); + # delete($defs{$hash->{NAME}}{READINGS}{AlarmListIDs}); + # delete($defs{$hash->{NAME}}{READINGS}{AlarmListVersion}); + #} + # + #return undef; +} + +######################################################################################## +# +# SONOSPLAYER_Get - Implements GetFn function +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## +sub SONOSPLAYER_Get($@) { + my ($hash, @a) = @_; + + my $reading = $a[1]; + my $name = $hash->{NAME}; + my $udn = $hash->{UDN}; + + # check argument + return "SONOSPLAYER: Get with unknown argument $a[1], choose one of ".join(" ", sort keys %gets) if(!defined($gets{$reading})); + + # some argument needs parameter(s), some not + return "SONOSPLAYER: $a[1] needs parameter(s): ".$gets{$a[1]} if (scalar(split(',', $gets{$a[1]})) > scalar(@a) - 2); + + # getter + if (lc($reading) eq 'currenttrackposition') { + SONOS_DoWork($udn, 'getCurrentTrackPosition'); + } elsif (lc($reading) eq 'playlists') { + SONOS_DoWork($udn, 'getPlaylists'); + } elsif (lc($reading) eq 'playlistswithcovers') { + SONOS_DoWork($udn, 'getPlaylistsWithCovers'); + } elsif (lc($reading) eq 'favourites') { + SONOS_DoWork($udn, 'getFavourites'); + } elsif (lc($reading) eq 'favouriteswithcovers') { + SONOS_DoWork($udn, 'getFavouritesWithCovers'); + } elsif (lc($reading) eq 'radios') { + SONOS_DoWork($udn, 'getRadios'); + } elsif (lc($reading) eq 'radioswithcovers') { + SONOS_DoWork($udn, 'getRadiosWithCovers'); + } elsif (lc($reading) eq 'ethernetportstatus') { + my $portNum = $a[2]; + + readingsSingleUpdate($hash, 'LastActionResult', 'Portstatus properly returned', 1); + + my $url = ReadingsVal($name, 'location', ''); + $url =~ s/(^http:\/\/.*?)\/.*/$1\/status\/enetports/; + + my $statusPage = GetFileFromURL($url); + return (($1 == 0) ? 'Inactive' : 'Active') if ($statusPage =~ m/<Port port='$portNum'><Link>(\d+)<\/Link><Speed>.*?<\/Speed><\/Port>/i); + return 'Inactive'; + } elsif (lc($reading) eq 'alarm') { + my $id = $a[2]; + + readingsSingleUpdate($hash, 'LastActionResult', 'Alarm-Hash properly returned', 1); + + my @idList = split(',', ReadingsVal($name, 'AlarmListIDs', '')); + if (!SONOS_isInList($id, @idList)) { + return {}; + } else { + return eval(ReadingsVal($name, 'AlarmList', ()))->{$id}; + } + } elsif (lc($reading) eq 'possibleroomicons') { + return join(', ', @possibleRoomIcons); + } + + return undef; +} + +####################################################################################### +# +# SONOSPLAYER_Set - Set one value for device +# +# Parameter hash = hash of device addressed +# a = argument string +# +######################################################################################## +sub SONOSPLAYER_Set($@) { + my ($hash, @a) = @_; + + # for the ?-selector: which values are possible + if($a[1] eq '?') { + # %setCopy enthält eine Kopie von %sets, da für eine ?-Anfrage u.U. ein Slider zurückgegeben werden muss... + my %setcopy; + if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) { + foreach my $key (keys %sets) { + my $oldkey = $key; + $key = $key.':slider,0,1,100' if ($key eq 'Volume'); + $key = $key.':slider,-100,1,100' if ($key eq 'Balance'); + + $setcopy{$key} = $sets{$oldkey}; + } + } else { + %setcopy = %sets; + } + + my $sonosDev = SONOS_getDeviceDefHash(undef); + $sets{Speak1} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak1', '') ne ''); + $sets{Speak2} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak2', '') ne ''); + $sets{Speak3} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak3', '') ne ''); + $sets{Speak4} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak4', '') ne ''); + + return join(" ", sort keys %setcopy); + } + + # check argument + return "SONOSPLAYER: Set with unknown argument $a[1], choose one of ".join(" ", sort keys %sets) if(!defined($sets{$a[1]})); + + # some argument needs parameter(s), some not + return "SONOSPLAYER: $a[1] needs parameter(s): ".$sets{$a[1]} if (scalar(split(',', $sets{$a[1]})) > scalar(@a) - 2); + + # define vars + my $key = $a[1]; + my $value = $a[2]; + my $value2 = $a[3]; + my $name = $hash->{NAME}; + my $udn = $hash->{UDN}; + + # setter + if (lc($key) eq 'currenttrackposition') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setCurrentTrackPosition', $value); + } elsif (lc($key) eq 'groupvolume') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + if ($value =~ m/^[+-]{1}/) { + SONOS_DoWork($udn, 'setRelativeGroupVolume', $value, $value2); + } else { + SONOS_DoWork($udn, 'setGroupVolume', $value, $value2); + } + } elsif (lc($key) eq 'snapshotgroupvolume') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setSnapshotGroupVolume', $value); + } elsif (lc($key) eq 'volume') { + if ($value =~ m/^[+-]{1}/) { + SONOS_DoWork($udn, 'setRelativeVolume', $value, $value2); + } else { + SONOS_DoWork($udn, 'setVolume', $value, $value2); + } + } elsif (lc($key) eq 'volumesave') { + setReadingsVal($hash, 'VolumeStore', ReadingsVal($name, 'Volume', 0), TimeNow()); + if ($value =~ m/^[+-]{1}/) { + SONOS_DoWork($udn, 'setRelativeVolume', $value); + } else { + SONOS_DoWork($udn, 'setVolume', $value); + } + } elsif (lc($key) eq 'volumerestore') { + SONOS_DoWork($udn, 'setVolume', ReadingsVal($name, 'VolumeStore', 0)); + } elsif (lc($key) eq 'volumed') { + SONOS_DoWork($udn, 'setRelativeVolume', -AttrVal($hash->{NAME}, 'VolumeStep', 7)); + } elsif (lc($key) eq 'volumeu') { + SONOS_DoWork($udn, 'setRelativeVolume', AttrVal($hash->{NAME}, 'VolumeStep', 7)); + } elsif (lc($key) eq 'balance') { + SONOS_DoWork($udn, 'setBalance', $value); + } elsif (lc($key) eq 'loudness') { + SONOS_DoWork($udn, 'setLoudness', $value); + } elsif (lc($key) eq 'bass') { + SONOS_DoWork($udn, 'setBass', $value); + } elsif (lc($key) eq 'treble') { + SONOS_DoWork($udn, 'setTreble', $value); + } elsif (lc($key) eq 'groupmute') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setGroupMute', $value); + } elsif (lc($key) eq 'mute') { + SONOS_DoWork($udn, 'setMute', $value); + } elsif (lc($key) eq 'mutet') { + SONOS_DoWork($udn, 'setMuteT', ''); + } elsif (lc($key) eq 'shuffle') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setShuffle', $value); + } elsif (lc($key) eq 'repeat') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setRepeat', $value); + } elsif (lc($key) eq 'crossfademode') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setCrossfadeMode', $value); + } elsif (lc($key) eq 'ledstate') { + SONOS_DoWork($udn, 'setLEDState', $value); + } elsif (lc($key) eq 'play') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'play'); + } elsif (lc($key) eq 'stop') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'stop'); + } elsif (lc($key) eq 'pause') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'pause'); + } elsif (lc($key) eq 'previous') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'previous'); + } elsif (lc($key) eq 'next') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'next'); + } elsif (lc($key) eq 'track') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setTrack', $value); + } elsif (lc($key) eq 'loadradio') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'loadRadio', $value); + } elsif (lc($key) eq 'startradio') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'loadRadio', $value); + SONOS_DoWork($udn, 'play'); + } elsif (lc($key) eq 'startfavourite') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'startFavourite', $value, $value2); + } elsif (lc($key) eq 'loadplaylist') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + $value2 = 1 if (!defined($value2)); + + if ($value =~ m/^file:(.*)/) { + SONOS_DoWork($udn, 'loadPlaylist', ':m3ufile:'.$1, $value2); + } else { + SONOS_DoWork($udn, 'loadPlaylist', $value, $value2); + } + } elsif (lc($key) eq 'startplaylist') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + $value2 = 1 if (!defined($value2)); + + if ($value =~ m/^file:(.*)/) { + SONOS_DoWork($udn, 'loadPlaylist', ':m3ufile:'.$1, $value2); + } else { + SONOS_DoWork($udn, 'loadPlaylist', $value, $value2); + } + SONOS_DoWork($udn, 'play'); + } elsif (lc($key) eq 'emptyplaylist') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'emptyPlaylist'); + } elsif (lc($key) eq 'saveplaylist') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + if ($value =~ m/^file:(.*)/) { + SONOS_DoWork($udn, 'savePlaylist', $1, ':m3ufile:'); + } else { + SONOS_DoWork($udn, 'savePlaylist', $value, ''); + } + } elsif (lc($key) eq 'currentplaylist') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setCurrentPlaylist'); + } elsif (lc($key) eq 'createthemelist') { + SONOS_DoWork($udn, 'createThemelist'); + } elsif (lc($key) eq 'playuri') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + # Prüfen, ob ein Sonosplayer-Device angegeben wurde, dann diesen AV Eingang als Quelle wählen + my $dHash = SONOS_getDeviceDefHash($value); + if (defined($dHash)) { + my $udnShort = $1 if ($dHash->{UDN} =~ m/(.*)_MR/); + + # Wenn dieses Quell-Device eine Playbar ist, dann den optischen Eingang als Quelle wählen... + if (ReadingsVal($dHash->{NAME}, 'playerType', '') eq 'S9') { + # Das ganze geht nur bei dem eigenen Eingang, ansonsten eine Gruppenwiedergabe starten + if ($dHash->{NAME} eq $hash->{NAME}) { + $value = 'x-sonos-htastream:'.$udnShort.':spdif'; + } else { + # Auf dem anderen Player den TV-Eingang wählen + SONOS_DoWork($dHash->{UDN}, 'playURI', 'x-sonos-htastream:'.$udnShort.':spdif', undef); + + # Gruppe bilden + SONOS_DoWork($hash->{UDN}, 'playURI', 'x-rincon:'.$udnShort, $value2); + + # Wir sind hier fertig + return undef; + } + } else { + $value = 'x-rincon-stream:'.$udnShort; + } + } + + SONOS_DoWork($udn, 'playURI', $value, $value2); + } elsif (lc($key) eq 'playuritemp') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'playURITemp', $value, $value2); + } elsif (lc($key) eq 'adduritoqueue') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'addURIToQueue', $value); + } elsif ((lc($key) eq 'speak') || ($key =~ m/speak\d+/i)) { + $key = 'speak0' if (lc($key) eq 'speak'); + + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + # Hier die komplette restliche Zeile in den zweiten Parameter packen, da damit auch Leerzeichen möglich sind + my $text = ''; + for(my $i = 4; $i < @a; $i++) { + $text .= ' '.$a[$i]; + } + SONOS_DoWork($udn, lc($key), $value, $value2, $text); + } elsif (lc($key) eq 'alarm') { + # Hier die komplette restliche Zeile in den zweiten Parameter packen, da damit auch Leerzeichen möglich sind + my $text = ''; + for(my $i = 4; $i < @a; $i++) { + $text .= ' '.$a[$i]; + } + + SONOS_DoWork($udn, 'setAlarm', $value, $value2, $text); + } elsif (lc($key) eq 'dailyindexrefreshtime') { + SONOS_DoWork($udn, 'setDailyIndexRefreshTime', $value); + } elsif (lc($key) eq 'sleeptimer') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setSleepTimer', $value); + } elsif (lc($key) eq 'addmember') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + my $cHash = SONOS_getDeviceDefHash($value); + if ($cHash) { + SONOS_DoWork($udn, 'addMember', $cHash->{UDN}); + } else { + my @sonosDevs = (); + foreach my $dev (SONOS_getAllSonosplayerDevices()) { + push(@sonosDevs, $dev->{NAME}) if ($dev->{NAME} ne $hash->{NAME}); + } + readingsSingleUpdate($hash, 'LastActionResult', 'AddMember: Wrong Sonos-Devicename "'.$value.'". Use one of "'.join('", "', @sonosDevs).'"', 1); + + return undef; + } + } elsif (lc($key) eq 'removemember') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + my $cHash = SONOS_getDeviceDefHash($value); + if ($cHash) { + SONOS_DoWork($udn, 'removeMember', $cHash->{UDN}); + } else { + my @sonosDevs = (); + foreach my $dev (SONOS_getAllSonosplayerDevices()) { + push(@sonosDevs, $dev->{NAME}) if ($dev->{NAME} ne $hash->{NAME}); + } + readingsSingleUpdate($hash, 'LastActionResult', 'RemoveMember: Wrong Sonos-Devicename "'.$value.'". Use one of "'.join('", "', @sonosDevs).'"', 1); + + return undef; + } + } elsif (lc($key) eq 'reboot') { + readingsSingleUpdate($hash, 'LastActionResult', 'Reboot properly initiated', 1); + + my $url = ReadingsVal($name, 'location', ''); + $url =~ s/(^http:\/\/.*?)\/.*/$1\/reboot/; + + GetFileFromURL($url); + } elsif (lc($key) eq 'wifi') { + $value = lc($value); + if ($value ne 'on' && $value ne 'off' && $value ne 'persist-off') { + readingsSingleUpdate($hash, 'LastActionResult', 'Wrong parameter "'.$value.'". Use one of "off", "persist-off" or "on".', 1); + + return undef; + } + + readingsSingleUpdate($hash, 'LastActionResult', 'WiFi properly set to '.$value, 1); + + my $url = ReadingsVal($name, 'location', ''); + $url =~ s/(^http:\/\/.*?)\/.*/$1\/wifictrl?wifi=$value/; + + GetFileFromURL($url); + } elsif (lc($key) eq 'name') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + # Hier die komplette restliche Zeile in den Parameter packen, da damit auch Leerzeichen möglich sind + my $text = ''; + for(my $i = 2; $i < @a; $i++) { + $text .= ' '.$a[$i]; + } + + SONOS_DoWork($udn, 'setName', $text); + } elsif (lc($key) eq 'icon') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + $value = lc($value); + + if (SONOS_posInList($value, @possibleRoomIcons) != -1) { + SONOS_DoWork($udn, 'setIcon', $value); + } else { + return 'Wrong icon name. Use one of "'.join('", "', @possibleRoomIcons).'".'; + } + } else { + return 'Not implemented yet!'; + } + + return (undef, 1); +} + +######################################################################################## +# +# SONOSPLAYER_GetRealTargetPlayerHash - Retreives the Real Player Hash for Device-Commands +# In Case of no grouping: the given hash (the normal device) +# In Case of grouping: the hash of the groupmaster +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOSPLAYER_GetRealTargetPlayerHash($) { + my ($hash) = @_; + + my $udnShort = $1 if ($hash->{UDN} =~ m/(.*)_MR/); + + my $targetUDNShort = $udnShort; + $targetUDNShort = $1 if (ReadingsVal($hash->{NAME}, 'ZoneGroupID', '') =~ m/(.*?):/); + + return SONOS_getSonosPlayerByUDN($targetUDNShort.'_MR') if ($udnShort ne $targetUDNShort); + return $hash; +} + +######################################################################################## +# +# SONOSPLAYER_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +######################################################################################## +sub SONOSPLAYER_Undef ($) { + my ($hash) = @_; + + RemoveInternalTimer($hash); + + return undef; +} + +######################################################################################## +# +# SONOSPLAYER_Log - Log to the normal Log-command with additional Infomations like Thread-ID and the prefix 'SONOSPLAYER' +# +######################################################################################## +sub SONOSPLAYER_Log($$$) { + my ($devicename, $level, $text) = @_; + + Log3 $devicename, $level, 'SONOSPLAYER'.threads->tid().': '.$text; +} + +1; + +=pod +=begin html + +<a name="SONOSPLAYER"></a> +<h3>SONOSPLAYER</h3> +<p>FHEM module to work with a Sonos Zoneplayer</p> +<p>For more informations have also a closer look at the wiki at <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a></p> +<p>Normally you don't have to define a Sonosplayer-Device on your own, because the Sonos-Device will do this for you during the discovery-process.</p> +<h4>Example</h4> +<p> +<code>define Sonos_Wohnzimmer SONOSPLAYER RINCON_000EFEFEFEF401400_MR</code> +</p> +<br /> +<a name="SONOSPLAYERdefine"></a> +<h4>Define</h4> +<code>define <name> SONOSPLAYER <udn></code> +<p> +<code><udn></code><br /> MAC-Address based identifier of the zoneplayer</p> +<br /> +<br /> +<a name="SONOSPLAYERset"></a> +<h4>Set</h4> +<ul> +<li><b>Common Tasks</b><ul> +<li><a name="SONOSPLAYER_setter_Alarm"> +<code>set <name> Alarm (Create|Update|Delete) <ID> <Datahash></code></a> +<br />Can be used for working on alarms:<ul><li><b>Create:</b> Creates an alarm-entry with the given datahash.</li><li><b>Update:</b> Updates the alarm-entry with the given id and datahash.</li><li><b>Delete:</b> Deletes the alarm-entry with the given id.</li></ul><br /><b>The Datahash:</b><br />The Format is a perl-hash and is interpreted with the eval-function.<br />e.g.: { Repeat => 1 }<br /><br />The following entries are allowed/neccessary:<ul><li>StartTime</li><li>Duration</li><li>Recurrence_Once</li><li>Recurrence_Monday</li><li>Recurrence_Tuesday</li><li>Recurrence_Wednesday</li><li>Recurrence_Thursday</li><li>Recurrence_Friday</li><li>Recurrence_Saturday</li><li>Recurrence_Sunday</li><li>Enabled</li><li>ProgramURI</li><li>ProgramMetaData</li><li>Shuffle</li><li>Repeat</li><li>Volume</li><li>IncludeLinkedZones</li></ul><br />e.g.:<ul><li>set Sonos_Wohnzimmer Alarm Create 0 { Enabled => 1, Volume => 35, StartTime => '00:00:00', Duration => '00:15:00', Repeat => 0, Shuffle => 0, ProgramURI => 'x-rincon-buzzer:0', ProgramMetaData => '', Recurrence_Once => 0, Recurrence_Monday => 1, Recurrence_Tuesday => 1, Recurrence_Wednesday => 1, Recurrence_Thursday => 1, Recurrence_Friday => 1, Recurrence_Saturday => 0, Recurrence_Sunday => 0, IncludeLinkedZones => 0 }</li><li>set Sonos_Wohnzimmer Alarm Update 17 { Shuffle => 1 }</li><li>set Sonos_Wohnzimmer Alarm Delete 17 {}</li></ul></li> +<li><a name="SONOSPLAYER_setter_DailyIndexRefreshTime"> +<code>set <name> DailyIndexRefreshTime <time></code></a> +<br />Sets the current DailyIndexRefreshTime for the whole bunch of Zoneplayers.</li> +<li><a name="SONOSPLAYER_setter_Icon"> +<code>set <name> Icon <Iconname></code></a> +<br />Sets the Icon for this Zone</li> +<li><a name="SONOSPLAYER_setter_Name"> +<code>set <name> Name <Zonename></code></a> +<br />Sets the Name for this Zone</li> +<li><a name="SONOSPLAYER_setter_Reboot"> +<code>set <name> Reboot</code></a> +<br />Initiates a reboot on the Zoneplayer.</li> +<li><a name="SONOSPLAYER_setter_Wifi"> +<code>set <name> Wifi <State></code></a> +<br />Sets the WiFi-State of the given Player. Can be 'off', 'persist-off' or 'on'.</li> +</ul></li> +<li><b>Playing Control-Commands</b><ul> +<li><a name="SONOSPLAYER_setter_CurrentTrackPosition"> +<code>set <name> CurrentTrackPosition <TimePosition></code></a> +<br /> Sets the current timeposition inside the title to the given value.</li> +<li><a name="SONOSPLAYER_setter_Pause"> +<code>set <name> Pause</code></a> +<br /> Pause the playing</li> +<li><a name="SONOSPLAYER_setter_Previous"> +<code>set <name> Previous</code></a> +<br /> Jumps to the beginning of the previous title.</li> +<li><a name="SONOSPLAYER_setter_Play"> +<code>set <name> Play</code></a> +<br /> Starts playing</li> +<li><a name="SONOSPLAYER_setter_PlayURI"> +<code>set <name> PlayURI <songURI> [Volume]</code></a> +<br />Plays the given MP3-File with the optional given volume.</li> +<li><a name="SONOSPLAYER_setter_PlayURITemp"> +<code>set <name> PlayURITemp <songURI> [Volume]</code></a> +<br />Plays the given MP3-File with the optional given volume as a temporary file. After playing it, the whole state is reconstructed and continues playing at the former saved position and volume and so on. If the file given is a stream (exactly: a file where the running time could not be determined), the call would be identical to <code>,PlayURI</code>, e.g. nothing is restored after playing.</li> +<li><a name="SONOSPLAYER_setter_Next"> +<code>set <name> Next</code></a> +<br /> Jumps to the beginning of the next title</li> +<li><a name="SONOSPLAYER_setter_Speak"> +<code>set <name> Speak <Volume> <Language> <Text></code></a> +<br />Uses the Google Text-To-Speech-Engine for generating MP3-Files of the given text and plays it on the SonosPlayer. Possible languages can be obtained from Google. e.g. "de", "en", "fr", "es"...</li> +<li><a name="SONOSPLAYER_setter_StartFavourite"> +<code>set <name> StartFavourite <Favouritename> [NoStart]</code></a> +<br /> Starts the named sonos-favorite. The parameter should be URL-encoded for proper naming of lists with special characters. If the Word 'NoStart' is given as second parameter, than the Loading will be done, but the playing-state is leaving untouched e.g. not started.</li> +<li><a name="SONOSPLAYER_setter_StartPlaylist"> +<code>set <name> StartPlaylist <Playlistname> [EmptyQueueBeforeImport]</code></a> +<br /> Loads the given Playlist and starts playing immediately. For all Options have a look at "LoadPlaylist".</li> +<li><a name="SONOSPLAYER_setter_StartRadio"> +<code>set <name> StartRadio <Radiostationname></code></a> +<br /> Loads the named radiostation (favorite) and starts playing immediately. For all Options have a look at "LoadRadio".</li> +<li><a name="SONOSPLAYER_setter_Stop"> +<code>set <name> Stop</code></a> +<br /> Stops the playing</li> +<li><a name="SONOSPLAYER_setter_Track"> +<code>set <name> Track <TrackNumber|Random></code></a> +<br /> Sets the track with the given tracknumber as the current title. If the tracknumber is the word <code>Random</code> a random track will be selected.</li> +</ul></li> +<li><b>Playing Settings</b><ul> +<li><a name="SONOSPLAYER_setter_Balance"> +<code>set <name> Balance <BalanceValue></code></a> +<br /> Sets the balance to the given value. The value can range from -100 (full left) to 100 (full right). Retrieves the new balancevalue as the result.</li> +<li><a name="SONOSPLAYER_setter_Bass"> +<code>set <name> Bass <BassValue></code></a> +<br /> Sets the bass to the given value. The value can range from -10 to 10. Retrieves the new bassvalue as the result.</li> +<li><a name="SONOSPLAYER_setter_CrossfadeMode"> +<code>set <name> CrossfadeMode <State></code></a> +<br /> Sets the crossfade-mode. Retrieves the new mode as the result.</li> +<li><a name="SONOSPLAYER_setter_LEDState"> +<code>set <name> LEDState <State></code></a> +<br /> Sets the LED state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_Loudness"> +<code>set <name> Loudness <State></code></a> +<br /> Sets the loudness-state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_Mute"> +<code>set <name> Mute <State></code></a> +<br /> Sets the mute-state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_MuteT"> +<code>set <name> MuteT</code></a> +<br /> Toggles the mute state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_Repeat"> +<code>set <name> Repeat <State></code></a> +<br /> Sets the repeat-state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_Shuffle"> +<code>set <name> Shuffle <State></code></a> +<br /> Sets the shuffle-state. Retrieves the new state as the result.</li> +<li><a name="SONOSPLAYER_setter_SleepTimer"> +<code>set <name> SleepTimer <Time></code></a> +<br /> Sets the Sleeptimer to the given Time. It must be in the full format of "HH:MM:SS". Deactivate with "00:00:00" or "off".</li> +<li><a name="SONOSPLAYER_setter_Treble"> +<code>set <name> Treble <TrebleValue></code></a> +<br /> Sets the treble to the given value. The value can range from -10 to 10. Retrieves the new treblevalue as the result.</li> +<li><a name="SONOSPLAYER_setter_Volume"> +<code>set <name> Volume <VolumeLevel> [RampType]</code></a> +<br /> Sets the volume to the given value. The value could be a relative value with + or - sign. In this case the volume will be increased or decreased according to this value. Retrieves the new volume as the result.<br />Optional can be a RampType defined with a value between 1 and 3 which describes different templates defined by the Sonos-System.</li> +<li><a name="SONOSPLAYER_setter_VolumeD"> +<code>set <name> VolumeD</code></a> +<br /> Turns the volume by volumeStep-ticks down.</li> +<li><a name="SONOSPLAYER_setter_VolumeRestore"> +<code>set <name> VolumeRestore</code></a> +<br /> Restores the volume of a formerly saved volume.</li> +<li><a name="SONOSPLAYER_setter_VolumeSave"> +<code>set <name> VolumeSave <VolumeLevel></code></a> +<br /> Sets the volume to the given value. The value could be a relative value with + or - sign. In this case the volume will be increased or decreased according to this value. Retrieves the new volume as the result. Additionally it saves the old volume to a reading for restoreing.</li> +<li><a name="SONOSPLAYER_setter_VolumeU"> +<code>set <name> VolumeU</code></a> +<br /> Turns the volume by volumeStep-ticks up.</li> +</ul></li> +<li><b>Control the current Playlist</b><ul> +<li><a name="SONOSPLAYER_setter_AddURIToQueue"> +<code>set <name> AddURIToQueue <songURI></code></a> +<br />Adds the given MP3-File at the current position into the queue.</li> +<li><a name="SONOSPLAYER_setter_CurrentPlaylist"> +<code>set <name> CurrentPlaylist</code></a> +<br /> Sets the current playing to the current queue, but doesn't start playing (e.g. after hearing of a radiostream, where the current playlist still exists but is currently "not in use")</li> +<li><a name="SONOSPLAYER_setter_EmptyPlaylist"> +<code>set <name> EmptyPlaylist</code></a> +<br /> Clears the current queue</li> +<li><a name="SONOSPLAYER_setter_LoadPlaylist"> +<code>set <name> LoadPlaylist <Playlistname> [EmptyQueueBeforeImport]</code></a> +<br /> Loads the named playlist to the current playing queue. The parameter should be URL-encoded for proper naming of lists with special characters. The Playlistname can be a filename and then must be startet with 'file:' (e.g. 'file:c:/Test.m3u')<br />If EmptyQueueBeforeImport is given and set to 1, the queue will be emptied before the import process. If not given, the parameter will be interpreted as 1.</li> +<li><a name="SONOSPLAYER_setter_LoadRadio"> +<code>set <name> LoadRadio <Radiostationname></code></a> +<br /> Loads the named radiostation (favorite). The current queue will not be touched but deactivated. The parameter should be URL-encoded for proper naming of lists with special characters.</li> +<li><a name="SONOSPLAYER_setter_SavePlaylist"> +<code>set <name> SavePlaylist <Playlistname></code></a> +<br /> Saves the current queue as a playlist with the given name. An existing playlist with the same name will be overwritten. The parameter should be URL-encoded for proper naming of lists with special characters. The Playlistname can be a filename and then must be startet with 'file:' (e.g. 'file:c:/Test.m3u')</li> +</ul></li> +<li><b>Groupcontrol</b><ul> +<li><a name="SONOSPLAYER_setter_AddMember"> +<code>set <name> AddMember <devicename></code></a> +<br />Adds the given devicename to the current device as a groupmember. The current playing of the current device goes on and will be transfered to the given device (the new member).</li> +<li><a name="SONOSPLAYER_setter_GroupMute"> +<code>set <name> GroupMute <State></code></a> +<br />Sets the mute state of the complete group in one step. The value can be on or off.</li> +<li><a name="SONOSPLAYER_setter_GroupVolume"> +<code>set <name> GroupVolume <VolumeLevel></code></a> +<br />Sets the group-volume in the way the original controller does. This means, that the relative volumelevel between the different players will be saved during change.</li> +<li><a name="SONOSPLAYER_setter_RemoveMember"> +<code>set <name> RemoveMember <devicename></code></a> +<br />Removes the given device, so that they both are not longer a group. The current playing of the current device goes on normally. The cutted device stops his playing and has no current playlist anymore (since Sonos Version 4.2 the old playlist will be restored).</li> +<li><a name="SONOSPLAYER_setter_SnapshotGroupVolume"> +<code>set <name> SnapshotGroupVolume</code></a> +<br /> Save the current volume-relation of all players of the same group. It's neccessary for the use of "GroupVolume" and is stored until the next call of "SnapshotGroupVolume".</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERget"></a> +<h4>Get</h4> +<ul> +<li><b>Common</b><ul> +<li><a name="SONOSPLAYER_getter_Alarm"> +<code>get <name> Alarm <ID></code></a> +<br /> It's an exception to the normal getter semantics. Returns directly a Perl-Hash with the Alarm-Informations to the given id. It's just a shorthand for <code>eval(ReadingsVal(<Devicename>, 'Alarmlist', ()))->{<ID>};</code>.</li> +<li><a name="SONOSPLAYER_getter_EthernetPortStatus"> +<code>get <name> EthernetPortStatus <PortNumber></code></a> +<br /> Gets the Ethernet-Portstatus of the given Port. Can be 'Active' or 'Inactive'.</li> +<li><a name="SONOSPLAYER_getter_PossibleRoomIcons"> +<code>get <name> PossibleRoomIcons</code></a> +<br /> Retreives a list of all possible Roomiconnames for the use with "set Icon".</li> +</ul></li> +<li><b>Lists</b><ul> +<li><a name="SONOSPLAYER_getter_Favourites"> +<code>get <name> Favourites</code></a> +<br /> Retrieves a list with the names of all sonos favourites. This getter retrieves the same list on all Zoneplayer. The format is a comma-separated list with quoted names of favourites. e.g. "Liste 1","Entry 2","Test"</li> +<li><a name="SONOSPLAYER_getter_FavouritesWithCovers"> +<code>get <name> FavouritesWithCovers</code></a> +<br /> Retrieves a list with the stringrepresentation of a perl-hash which can easily be converted with "eval". It consists of the names and coverlinks of all of the favourites stored in Sonos e.g. {'FV:2/22' => {'Cover' => 'urlzumcover', 'Title' => '1. Favorit'}}</li> +<li><a name="SONOSPLAYER_getter_Playlists"> +<code>get <name> Playlists</code></a> +<br /> Retrieves a list with the names of all saved queues (aka playlists). This getter retrieves the same list on all Zoneplayer. The format is a comma-separated list with quoted names of playlists. e.g. "Liste 1","Liste 2","Test"</li> +<li><a name="SONOSPLAYER_getter_PlaylistsWithCovers"> +<code>get <name> PlaylistsWithCovers</code></a> +<br /> Retrieves a list with the stringrepresentation of a perl-hash which can easily be converted with "eval". It consists of the names and coverlinks of all of the playlists stored in Sonos e.g. {'SQ:14' => {'Cover' => 'urlzumcover', 'Title' => '1. Playlist'}}</li> +<li><a name="SONOSPLAYER_getter_Radios"> +<code>get <name> Radios</code></a> +<br /> Retrieves a list woth the names of all saved radiostations (favorites). This getter retrieves the same list on all Zoneplayer. The format is a comma-separated list with quoted names of radiostations. e.g. "Sender 1","Sender 2","Test"</li> +<li><a name="SONOSPLAYER_getter_RadiosWithCovers"> +<code>get <name> RadiosWithCovers</code></a> +<br /> Retrieves a list with the stringrepresentation of a perl-hash which can easily be converted with "eval". It consists of the names and coverlinks of all of the radiofavourites stored in Sonos e.g. {'R:0/0/2' => {'Cover' => 'urlzumcover', 'Title' => '1. Radiosender'}}</li> +</ul></li> +<li><b>Informations on the current Title</b><ul> +<li><a name="SONOSPLAYER_getter_CurrentTrackPosition"> +<code>get <name> CurrentTrackPosition</code></a> +<br /> Retrieves the current timeposition inside a title</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERattr"></a> +<h4>Attributes</h4> +<ul> +<li><b>Common</b><ul> +<li><a name="SONOSPLAYER_attribut_disable"><code>attr <name> disable <int></code> +</a><br /> One of (0,1). Disables the event-worker for this Sonosplayer.</li> +<li><a name="SONOSPLAYER_attribut_generateSomethingChangedEvent"><code>attr <name> generateSomethingChangedEvent <int></code> +</a><br /> One of (0,1). 1 if a 'SomethingChanged'-Event should be generated. This event is thrown every time an event is generated. This is useful if you wants to be notified on every change with a single event.</li> +<li><a name="SONOSPLAYER_attribut_generateVolumeEvent"><code>attr <name> generateVolumeEvent <int></code> +</a><br /> One of (0,1). Enables an event generated at volumechanges if minVolume or maxVolume is set.</li> +<li><a name="SONOSPLAYER_attribut_generateVolumeSlider"><code>attr <name> generateVolumeSlider <int></code> +</a><br /> One of (0,1). Enables a slider for volumecontrol in detail view.</li> +<li><a name="SONOSPLAYER_attribut_getAlarms"><code>attr <name> getAlarms <int></code> +</a><br /> One of (0..1). Initializes a callback-method for Alarms. This included the information of the DailyIndexRefreshTime.</li> +<li><a name="SONOSPLAYER_attribut_volumeStep"><code>attr <name> volumeStep <int></code> +</a><br /> One of (0..100). Defines the stepwidth for subsequent calls of <code>VolumeU</code> and <code>VolumeD</code>.</li> +</ul></li> +<li><b>Information Generation</b><ul> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize1"><code>attr <name> generateInfoSummarize1 <string></code> +</a><br /> Generates the reading 'InfoSummarize1' with the given format. More Information on this in the examples-section.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize2"><code>attr <name> generateInfoSummarize2 <string></code> +</a><br /> Generates the reading 'InfoSummarize2' with the given format. More Information on this in the examples-section.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize3"><code>attr <name> generateInfoSummarize3 <string></code> +</a><br /> Generates the reading 'InfoSummarize3' with the given format. More Information on this in the examples-section.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize4"><code>attr <name> generateInfoSummarize4 <string></code> +</a><br /> Generates the reading 'InfoSummarize4' with the given format. More Information on this in the examples-section.</li> +<li><a name="SONOSPLAYER_attribut_stateVariable"><code>attr <name> stateVariable <string></code> +</a><br /> One of (TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackURI,nextAlbumArtURI,nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,Shuffle,Repeat,CrossfadeMode,Balance,HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,InfoSummarize2,InfoSummarize3,InfoSummarize4). Defines, which variable has to be copied to the content of the state-variable.</li> +</ul></li> +<li><b>Controloptions</b><ul> +<li><a name="SONOSPLAYER_attribut_maxVolume"><code>attr <name> maxVolume <int></code> +</a><br /> One of (0..100). Define a maximal volume for this Zoneplayer</li> +<li><a name="SONOSPLAYER_attribut_minVolume"><code>attr <name> minVolume <int></code> +</a><br /> One of (0..100). Define a minimal volume for this Zoneplayer</li> +<li><a name="SONOSPLAYER_attribut_maxVolumeHeadphone"><code>attr <name> maxVolumeHeadphone <int></code> +</a><br /> One of (0..100). Define a maximal volume for this Zoneplayer for use with headphones</li> +<li><a name="SONOSPLAYER_attribut_minVolumeHeadphone"><code>attr <name> minVolumeHeadphone <int></code> +</a><br /> One of (0..100). Define a minimal volume for this Zoneplayer for use with headphones</li> +<li><a name="SONOSPLAYER_attribut_buttonEvents"><code>attr <name> buttonEvents <Time:Pattern>[ <Time:Pattern> ...]</code> +</a><br /> Defines that after pressing a specified sequence of buttons at the player an event has to be thrown. The definition itself is a tupel: the first part (before the colon) is the time in seconds, the second part (after the colon) is the button sequence of this event.<br /> +The following button-shortcuts are possible: <ul><li><b>M</b>: The Mute-Button</li><li><b>H</b>: The Headphone-Connector</li><li><b>U</b>: Up-Button (Volume Up)</li><li><b>D</b>: Down-Button (Volume Down)</li></ul><br /> +The event thrown is named <code>ButtonEvent</code>, the value is the defined button-sequence.<br /> +E.G.: <code>2:MM</code><br /> +Here an event is defined, where in time of 2 seconds the Mute-Button has to be pressed 2 times. The created event is named <code>ButtonEvent</code> and has the value <code>MM</code>.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERexamples"></a> +<h4>Examples / Tips</h4> +<ul> +<li><a name="SONOSPLAYER_examples_InfoSummarize">Format of InfoSummarize:</a><br /> +<code>infoSummarizeX := <NormalAudio>:summarizeElem:</NormalAudio> <StreamAudio>:summarizeElem:</StreamAudio>|:summarizeElem:</code><br /> +<code>:summarizeElem: := <:variable:[ prefix=":text:"][ suffix=":text:"][ instead=":text:"][ ifempty=":text:"]/[ emptyVal=":text:"]></code><br /> +<code>:variable: := TransportState|NumberOfTracks|Track|TrackURI|TrackDuration|Title|Artist|Album|OriginalTrackNumber|AlbumArtist|Sender|SenderCurrent|SenderInfo|StreamAudio|NormalAudio|AlbumArtURI|nextTrackDuration|nextTrackURI|nextAlbumArtURI|nextTitle|nextArtist|nextAlbum|nextAlbumArtist|nextOriginalTrackNumber|Volume|Mute|Shuffle|Repeat|CrossfadeMode|Balance|HeadphoneConnected|SleepTimer|Presence|RoomName|SaveRoomName|PlayerType|Location|SoftwareRevision|SerialNum|InfoSummarize1|InfoSummarize2|InfoSummarize3|InfoSummarize4</code><br /> +<code>:text: := [Any text without double-quotes]</code><br /></li> +</ul> + +=end html + +=begin html_DE + +<a name="SONOSPLAYER"></a> +<h3>SONOSPLAYER</h3> +<p>FHEM Modul für die Steuerung eines Sonos Zoneplayer</p> +<p>Für weitere Hinweise und Beschreibungen bitte auch im Wiki unter <a href="http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel">http://www.fhemwiki.de/wiki/Sonos_Anwendungsbeispiel</a> nachschauen.</p> +<p>Im Normalfall braucht man dieses Device nicht selber zu definieren, da es automatisch vom Discovery-Process des Sonos-Device erzeugt wird.</p> +<h4>Example</h4> +<p> +<code>define Sonos_Wohnzimmer SONOSPLAYER RINCON_000EFEFEFEF401400_MR</code> +</p> +<br /> +<a name="SONOSPLAYERdefine"></a> +<h4>Definition</h4> +<code>define <name> SONOSPLAYER <udn></code> +<p> +<code><udn></code><br /> MAC-Addressbasierter eindeutiger Bezeichner des Zoneplayer</p> +<p> +<br /> +<br /> +<a name="SONOSPLAYERset"></a> +<h4>Set</h4> +<ul> +<li><b>Grundsätzliche Einstellungen</b><ul> +<li><a name="SONOSPLAYER_setter_Alarm"> +<code>set <name> Alarm (Create|Update|Delete) <ID> <Datahash></code></a> +<br />Diese Anweisung wird für die Bearbeitung der Alarme verwendet:<ul><li><b>Create:</b> Erzeugt einen neuen Alarm-Eintrag mit den übergebenen Hash-Daten.</li><li><b>Update:</b> Aktualisiert den Alarm mit der übergebenen ID und den angegebenen Hash-Daten.</li><li><b>Delete:</b> Löscht den Alarm-Eintrag mit der übergebenen ID.</li></ul><br /><b>Die Hash-Daten:</b><br />Das Format ist ein Perl-Hash und wird mittels der eval-Funktion interpretiert.<br />e.g.: { Repeat => 1 }<br /><br />Die folgenden Schlüssel sind zulässig/notwendig:<ul><li>StartTime</li><li>Duration</li><li>Recurrence_Once</li><li>Recurrence_Monday</li><li>Recurrence_Tuesday</li><li>Recurrence_Wednesday</li><li>Recurrence_Thursday</li><li>Recurrence_Friday</li><li>Recurrence_Saturday</li><li>Recurrence_Sunday</li><li>Enabled</li><li>ProgramURI</li><li>ProgramMetaData</li><li>Shuffle</li><li>Repeat</li><li>Volume</li><li>IncludeLinkedZones</li></ul><br />z.B.:<ul><li>set Sonos_Wohnzimmer Alarm Create 0 { Enabled => 1, Volume => 35, StartTime => '00:00:00', Duration => '00:15:00', Repeat => 0, Shuffle => 0, ProgramURI => 'x-rincon-buzzer:0', ProgramMetaData => '', Recurrence_Once => 0, Recurrence_Monday => 1, Recurrence_Tuesday => 1, Recurrence_Wednesday => 1, Recurrence_Thursday => 1, Recurrence_Friday => 1, Recurrence_Saturday => 0, Recurrence_Sunday => 0, IncludeLinkedZones => 0 }</li><li>set Sonos_Wohnzimmer Alarm Update 17 { Shuffle => 1 }</li><li>set Sonos_Wohnzimmer Alarm Delete 17 {}</li></ul></li> +<li><a name="SONOSPLAYER_setter_DailyIndexRefreshTime"> +<code>set <name> DailyIndexRefreshTime <time></code></a> +<br />Setzt die aktuell gültige DailyIndexRefreshTime für alle Zoneplayer.</li> +<li><a name="SONOSPLAYER_setter_Icon"> +<code>set <name> Icon <Iconname></code></a> +<br />Legt das Icon für die Zone fest</li> +<li><a name="SONOSPLAYER_setter_Name"> +<code>set <name> Name <Zonename></code></a> +<br />Legt den Namen der Zone fest.</li> +<li><a name="SONOSPLAYER_setter_Reboot"> +<code>set <name> Reboot</code></a> +<br />Führt für den Zoneplayer einen Neustart durch.</li> +<li><a name="SONOSPLAYER_setter_Wifi"> +<code>set <name> Wifi <State></code></a> +<br />Setzt den WiFi-Zustand des Players. Kann 'off', 'persist-off' oder 'on' sein.</li> +</ul></li> +<li><b>Abspiel-Steuerbefehle</b><ul> +<li><a name="SONOSPLAYER_setter_CurrentTrackPosition"> +<code>set <name> CurrentTrackPosition <TimePosition></code></a> +<br /> Setzt die Abspielposition innerhalb des Liedes auf den angegebenen Zeitwert (z.B. 0:01:15).</li> +<li><a name="SONOSPLAYER_setter_Pause"> +<code>set <name> Pause</code></a> +<br /> Pausiert die Wiedergabe</li> +<li><a name="SONOSPLAYER_setter_Previous"> +<code>set <name> Previous</code></a> +<br /> Springt an den Anfang des vorherigen Titels.</li> +<li><a name="SONOSPLAYER_setter_Play"> +<code>set <name> Play</code></a> +<br /> Startet die Wiedergabe</li> +<li><a name="SONOSPLAYER_setter_PlayURI"> +<code>set <name> PlayURI <songURI> [Volume]</code></a> +<br /> Spielt die angegebene MP3-Datei ab. Dabei kann eine Lautstärke optional mit angegeben werden.</li> +<li><a name="SONOSPLAYER_setter_PlayURITemp"> +<code>set <name> PlayURITemp <songURI> [Volume]</code></a> +<br /> Spielt die angegebene MP3-Datei mit der optionalen Lautstärke als temporäre Wiedergabe ab. Nach dem Abspielen wird der vorhergehende Zustand wiederhergestellt, und läuft an der unterbrochenen Stelle weiter. Wenn die Länge der Datei nicht ermittelt werden kann (z.B. bei Streams), läuft die Wiedergabe genauso wie bei <code>PlayURI</code> ab, es wird also nichts am Ende (wenn es eines geben sollte) wiederhergestellt.</li> +<li><a name="SONOSPLAYER_setter_Next"> +<code>set <name> Next</code></a> +<br /> Springt an den Anfang des nächsten Titels</li> +<li><a name="SONOSPLAYER_setter_Speak"> +<code>set <name> Speak <Volume> <Language> <Text></code></a> +<br /> Verwendet die Google Text-To-Speech-Engine um den angegebenen Text in eine MP3-Datei umzuwandeln und anschließend mittels <code>PlayURITemp</code> als Durchsage abzuspielen. Mögliche Sprachen können auf der Google-Seite nachgesehen werden. Möglich sind z.B. "de", "en", "fr", "es"...</li> +<li><a name="SONOSPLAYER_setter_StartFavourite"> +<code>set <name> StartFavourite <FavouriteName> [NoStart]</code></a> +<br /> Startet den angegebenen Favoriten. Der Name bezeichnet einen Eintrag in der Sonos-Favoritenliste. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen. Wenn das Wort 'NoStart' als zweiter Parameter angegeben wurde, dann wird der Favorit geladen und fertig vorbereitet, aber nicht explizit gestartet.</li> +<li><a name="SONOSPLAYER_setter_StartPlaylist"> +<code>set <name> StartPlaylist <Playlistname> [EmptyQueueBeforeImport]</code></a> +<br /> Lädt die benannte Playlist und startet sofort die Wiedergabe. Zu den Parametern und Bemerkungen bitte unter "LoadPlaylist" nachsehen.</li> +<li><a name="SONOSPLAYER_setter_StartRadio"> +<code>set <name> StartRadio <Radiostationname></code></a> +<br /> Lädt den benannten Radiosender, genauer gesagt, den benannten Radiofavoriten und startet sofort die Wiedergabe. Dabei wird die bestehende Abspielliste beibehalten, aber deaktiviert. Der Parameter kann/muss URL-Encoded sein, um auch Leer- und Sonderzeichen angeben zu können.</li> +<li><a name="SONOSPLAYER_setter_Stop"> +<code>set <name> Stop</code></a> +<br /> Stoppt die Wiedergabe</li> +<li><a name="SONOSPLAYER_setter_Track"> +<code>set <name> Track <TrackNumber|Random></code></a> +<br /> Aktiviert den angebenen Titel der aktuellen Abspielliste. Wenn als Tracknummer der Wert <code>Random</code> angegeben wird, dann wird eine zufällige Trackposition ausgewählt.</li> +</ul></li> +<li><b>Einstellungen zum Abspielen</b><ul> +<li><a name="SONOSPLAYER_setter_Balance"> +<code>set <name> Balance <BalanceValue></code></a> +<br /> Setzt die Balance auf den angegebenen Wert. Der Wert kann zwischen -100 (voll links) bis 100 (voll rechts) sein. Gibt die wirklich eingestellte Balance als Ergebnis zurück.</li> +<li><a name="SONOSPLAYER_setter_Bass"> +<code>set <name> Bass <BassValue></code></a> +<br /> Setzt den Basslevel auf den angegebenen Wert. Der Wert kann zwischen -10 bis 10 sein. Gibt den wirklich eingestellten Basslevel als Ergebnis zurück.</li> +<li><a name="SONOSPLAYER_setter_CrossfadeMode"> +<code>set <name> CrossfadeMode <State></code></a> +<br /> Legt den Zustand des Crossfade-Mode fest. Liefert den aktuell gültigen Crossfade-Mode.</li> +<li><a name="SONOSPLAYER_setter_LEDState"> +<code>set <name> LEDState <State></code></a> +<br /> Legt den Zustand der LED fest. Liefert den aktuell gültigen Zustand.</li> +<li><a name="SONOSPLAYER_setter_Loudness"> +<code>set <name> Loudness <State></code></a> +<br /> Setzt den angegebenen Loudness-Zustand. Liefert den aktuell gültigen Loudness-Zustand.</li> +<li><a name="SONOSPLAYER_setter_Mute"> +<code>set <name> Mute <State></code></a> +<br /> Setzt den angegebenen Mute-Zustand. Liefert den aktuell gültigen Mute-Zustand.</li> +<li><a name="SONOSPLAYER_setter_MuteT"> +<code>set <name> MuteT</code></a> +<br /> Schaltet den Zustand des Mute-Zustands um. Liefert den aktuell gültigen Mute-Zustand.</li> +<li><a name="SONOSPLAYER_setter_Repeat"> +<code>set <name> Repeat <State></code></a> +<br /> Legt den Zustand des Repeat-Zustands fest. Liefert den aktuell gültigen Repeat-Zustand.</li> +<li><a name="SONOSPLAYER_setter_Shuffle"> +<code>set <name> Shuffle <State></code></a> +<br /> Legt den Zustand des Shuffle-Zustands fest. Liefert den aktuell gültigen Shuffle-Zustand.</li> +<li><a name="SONOSPLAYER_setter_SleepTimer"> +<code>set <name> SleepTimer <Time></code></a> +<br /> Legt den aktuellen SleepTimer fest. Der Wert muss ein kompletter Zeitstempel sein (HH:MM:SS). Zum Deaktivieren darf der Zeitstempel nur Nullen enthalten oder das Wort 'off'.</li> +<li><a name="SONOSPLAYER_setter_Treble"> +<code>set <name> Treble <TrebleValue></code></a> +<br /> Setzt den Treblelevel auf den angegebenen Wert. Der Wert kann zwischen -10 bis 10 sein. Gibt den wirklich eingestellten Treblelevel als Ergebnis zurück.</li> +<li><a name="SONOSPLAYER_setter_Volume"> +<code>set <name> Volume <VolumeLevel> [RampType]</code></a> +<br /> Setzt die aktuelle Lautstärke auf den angegebenen Wert. Der Wert kann ein relativer Wert mittels + oder - Zeichen sein. Liefert den aktuell gültigen Lautstärkewert zurück.<br />Optional kann ein RampType übergeben werden, der einen Wert zwischen 1 und 3 annehmen kann, und verschiedene von Sonos festgelegte Muster beschreibt.</li> +<li><a name="SONOSPLAYER_setter_VolumeD"> +<code>set <name> VolumeD</code></a> +<br /> Verringert die aktuelle Lautstärke um volumeStep-Einheiten.</li> +<li><a name="SONOSPLAYER_setter_VolumeRestore"> +<code>set <name> VolumeRestore</code></a> +<br /> Stellt die mittels <code>VolumeSave</code> gespeicherte Lautstärke wieder her.</li> +<li><a name="SONOSPLAYER_setter_VolumeSave"> +<code>set <name> VolumeSave <VolumeLevel></code></a> +<br /> Setzt die aktuelle Lautstärke auf den angegebenen Wert. Der Wert kann ein relativer Wert mittels + oder - Zeichen sein. Liefert den aktuell gültigen Lautstärkewert zurück. Zusätzlich wird der alte Lautstärkewert gespeichert und kann mittels <code>VolumeRestore</code> wiederhergestellt werden.</li> +<li><a name="SONOSPLAYER_setter_VolumeU"> +<code>set <name> VolumeU</code></a> +<br /> Erhöht die aktuelle Lautstärke um volumeStep-Einheiten.</li> +</ul></li> +<li><b>Steuerung der aktuellen Abspielliste</b><ul> +<li><a name="SONOSPLAYER_setter_AddURIToQueue"> +<code>set <name> AddURIToQueue <songURI></code></a> +<br /> Fügt die angegebene MP3-Datei an der aktuellen Stelle in die Abspielliste ein.</li> +<li><a name="SONOSPLAYER_setter_CurrentPlaylist"> +<code>set <name> CurrentPlaylist</code></a> +<br /> Setzt den Abspielmodus auf die aktuelle Abspielliste, startet aber keine Wiedergabe (z.B. nach dem Hören eines Radiostreams, wo die aktuelle Abspielliste noch existiert, aber gerade "nicht verwendet" wird)</li> +<li><a name="SONOSPLAYER_setter_EmptyPlaylist"> +<code>set <name> EmptyPlaylist</code></a> +<br /> Leert die aktuelle Abspielliste</li> +<li><a name="SONOSPLAYER_setter_LoadPlaylist"> +<code>set <name> LoadPlaylist <Playlistname> [EmptyQueueBeforeImport]</code></a> +<br /> Lädt die angegebene Playlist in die aktuelle Abspielliste. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen. Der Playlistname kann auch ein Dateiname sein. Dann muss dieser mit 'file:' beginnen (z.B. 'file:c:/Test.m3u).<br />Wenn der Parameter EmptyQueueBeforeImport mit ''1'' angegeben wirde, wird die aktuelle Abspielliste vor dem Import geleert. Standardmäßig wird hier ''1'' angenommen.</li> +<li><a name="SONOSPLAYER_setter_LoadRadio"> +<code>set <name> LoadRadio <Radiostationname></code></a> +<br /> Startet den angegebenen Radiostream. Der Name bezeichnet einen Sender in der Radiofavoritenliste. Die aktuelle Abspielliste wird nicht verändert. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen.</li> +<li><a name="SONOSPLAYER_setter_SavePlaylist"> +<code>set <name> SavePlaylist <Playlistname></code></a> +<br /> Speichert die aktuelle Abspielliste unter dem angegebenen Namen. Eine bestehende Playlist mit diesem Namen wird überschrieben. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen. Der Playlistname kann auch ein Dateiname sein. Dann muss dieser mit 'file:' beginnen (z.B. 'file:c:/Test.m3u).</li> +</ul></li> +<li><b>Gruppenbefehle</b><ul> +<li><a name="SONOSPLAYER_setter_AddMember"> +<code>set <name> AddMember <devicename></code></a> +<br />Fügt dem Device das übergebene Device als Gruppenmitglied hinzu. Die Wiedergabe des aktuellen Devices bleibt erhalten, und wird auf das angegebene Device mit übertragen.</li> +<li><a name="SONOSPLAYER_setter_GroupMute"> +<code>set <name> GroupMute <State></code></a> +<br />Setzt den Mute-Zustand für die komplette Gruppe in einem Schritt. Der Wert kann on oder off sein.</li> +<li><a name="SONOSPLAYER_setter_GroupVolume"> +<code>set <name> GroupVolume <VolumeLevel></code></a> +<br />Setzt die Gruppenlautstärke in der Art des Original-Controllers. Das bedeutet, dass das Lautstärkeverhältnis der Player zueinander beim Anpassen erhalten bleibt.</li> +<li><a name="SONOSPLAYER_setter_RemoveMember"> +<code>set <name> RemoveMember <devicename></code></a> +<br />Entfernt dem Device das übergebene Device, sodass die beiden keine Gruppe mehr bilden. Die Wiedergabe des aktuellen Devices läuft normal weiter. Das abgetrennte Device stoppt seine Wiedergabe, und hat keine aktuelle Abspielliste mehr (seit Sonos Version 4.2 hat der Player wieder die Playliste von vorher aktiv).</li> +<li><a name="SONOSPLAYER_setter_SnapshotGroupVolume"> +<code>set <name> SnapshotGroupVolume</code></a> +<br /> Legt das Lautstärkeverhältnis der aktuellen Player der Gruppe für folgende '''GroupVolume'''-Aufrufe fest. Dieses festgelegte Verhältnis wird bis zum nächsten Aufruf von '''SnapshotGroupVolume''' beibehalten.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERget"></a> +<h4>Get</h4> +<ul> +<li><b>Grundsätzliches</b><ul> +<li><a name="SONOSPLAYER_getter_Alarm"> +<code>get <name> Alarm <ID></code></a> +<br /> Ausnahmefall. Diese Get-Anweisung liefert direkt ein Hash zurück, in welchem die Informationen des Alarms mit der gegebenen ID enthalten sind. Es ist die Kurzform für <code>eval(ReadingsVal(<Devicename>, 'Alarmlist', ()))->{<ID>};</code>, damit sich nicht jeder ausdenken muss, wie er jetzt am einfachsten an die Alarm-Informationen rankommen kann.</li> +<li><a name="SONOSPLAYER_getter_EthernetPortStatus"> +<code>get <name> EthernetPortStatus <PortNumber></code></a> +<br /> Liefert den Ethernet-Portstatus des gegebenen Ports. Kann 'Active' oder 'Inactive' liefern.</li> +<li><a name="SONOSPLAYER_getter_PossibleRoomIcons"> +<code>get <name> PossibleRoomIcons</code></a> +<br /> Liefert eine Liste aller möglichen RoomIcon-Bezeichnungen zurück.</li> +</ul></li> +<li><b>Listen</b><ul> +<li><a name="SONOSPLAYER_getter_Favourites"> +<code>get <name> Favourites</code></a> +<br /> Liefert eine Liste mit den Namen aller gespeicherten Sonos-Favoriten. Das Format der Liste ist eine Komma-Separierte Liste, bei der die Namen in doppelten Anführungsstrichen stehen. z.B. "Liste 1","Eintrag 2","Test"</li> +<li><a name="SONOSPLAYER_getter_FavouritesWithCovers"> +<code>get <name> FavouritesWithCovers</code></a> +<br /> Liefert die Stringrepräsentation eines Hash mit den Namen und Covern aller gespeicherten Sonos-Favoriten. Z.B.: {'FV:2/22' => {'Cover' => 'urlzumcover', 'Title' => '1. Favorit'}}. Dieser String kann einfach mit '''eval''' in eine Perl-Datenstruktur umgewandelt werden.</li> +<li><a name="SONOSPLAYER_getter_Playlists"> +<code>get <name> Playlists</code></a> +<br /> Liefert eine Liste mit den Namen aller gespeicherten Playlists. Das Format der Liste ist eine Komma-Separierte Liste, bei der die Namen in doppelten Anführungsstrichen stehen. z.B. "Liste 1","Liste 2","Test"</li> +<li><a name="SONOSPLAYER_getter_PlaylistsWithCovers"> +<code>get <name> PlaylistsWithCovers</code></a> +<br /> Liefert die Stringrepräsentation eines Hash mit den Namen und Covern aller gespeicherten Sonos-Playlisten. Z.B.: {'SQ:14' => {'Cover' => 'urlzumcover', 'Title' => '1. Playlist'}}. Dieser String kann einfach mit '''eval''' in eine Perl-Datenstruktur umgewandelt werden.</li> +<li><a name="SONOSPLAYER_getter_Radios"> +<code>get <name> Radios</code></a> +<br /> Liefert eine Liste mit den Namen aller gespeicherten Radiostationen (Favoriten). Das Format der Liste ist eine Komma-Separierte Liste, bei der die Namen in doppelten Anführungsstrichen stehen. z.B. "Sender 1","Sender 2","Test"</li> +<li><a name="SONOSPLAYER_getter_RadiosWithCovers"> +<code>get <name> RadiosWithCovers</code></a> +<br /> Liefert die Stringrepräsentation eines Hash mit den Namen und Covern aller gespeicherten Sonos-Radiofavoriten. Z.B.: {'R:0/0/2' => {'Cover' => 'urlzumcover', 'Title' => '1. Radiosender'}}. Dieser String kann einfach mit '''eval''' in eine Perl-Datenstruktur umgewandelt werden.</li> +</ul></li> +<li><b>Informationen zum aktuellen Titel</b><ul> +<li><a name="SONOSPLAYER_getter_CurrentTrackPosition"> +<code>get <name> CurrentTrackPosition</code></a> +<br /> Liefert die aktuelle Position innerhalb des Titels.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERattr"></a> +<h4>Attribute</h4> +<ul> +<li><b>Grundsätzliches</b><ul> +<li><a name="SONOSPLAYER_attribut_disable"><code>attr <name> disable <int></code> +</a><br /> One of (0,1). Deaktiviert die Event-Verarbeitung für diesen Zoneplayer.</li> +<li><a name="SONOSPLAYER_attribut_generateSomethingChangedEvent"><code>attr <name> generateSomethingChangedEvent <int></code> +</a><br /> One of (0,1). 1 wenn ein 'SomethingChanged'-Event erzeugt werden soll. Dieses Event wird immer dann erzeugt, wenn sich irgendein Wert ändert. Dies ist nützlich, wenn man immer informiert werden möchte, egal, was sich geändert hat.</li> +<li><a name="SONOSPLAYER_attribut_generateVolumeEvent"><code>attr <name> generateVolumeEvent <int></code> +</a><br /> One of (0,1). Aktiviert die Generierung eines Events bei Lautstärkeänderungen, wenn minVolume oder maxVolume definiert sind.</li> +<li><a name="SONOSPLAYER_attribut_generateVolumeSlider"><code>attr <name> generateVolumeSlider <int></code> +</a><br /> One of (0,1). Aktiviert einen Slider für die Lautstärkekontrolle in der Detailansicht.</li> +<li><a name="SONOSPLAYER_attribut_getAlarms"><code>attr <name> getAlarms <int></code> +</a><br /> One of (0..1). Richtet eine Callback-Methode für Alarme ein. Damit wird auch die DailyIndexRefreshTime automatisch aktualisiert.</li> +<li><a name="SONOSPLAYER_attribut_volumeStep"><code>attr <name> volumeStep <int></code> +</a><br /> One of (0..100). Definiert die Schrittweite für die Aufrufe von <code>VolumeU</code> und <code>VolumeD</code>.</li> +</ul></li> +<li><b>Informationen generieren</b><ul> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize1"><code>attr <name> generateInfoSummarize1 <string></code> +</a><br /> Erzeugt das Reading 'InfoSummarize1' mit dem angegebenen Format. Mehr Informationen dazu im Bereich Beispiele.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize2"><code>attr <name> generateInfoSummarize2 <string></code> +</a><br /> Erzeugt das Reading 'InfoSummarize2' mit dem angegebenen Format. Mehr Informationen dazu im Bereich Beispiele.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize3"><code>attr <name> generateInfoSummarize3 <string></code> +</a><br /> Erzeugt das Reading 'InfoSummarize3' mit dem angegebenen Format. Mehr Informationen dazu im Bereich Beispiele.</li> +<li><a name="SONOSPLAYER_attribut_generateInfoSummarize4"><code>attr <name> generateInfoSummarize4 <string></code> +</a><br /> Erzeugt das Reading 'InfoSummarize4' mit dem angegebenen Format. Mehr Informationen dazu im Bereich Beispiele.</li> +<li><a name="SONOSPLAYER_attribut_stateVariable"><code>attr <name> stateVariable <string></code> +</a><br /> One of (TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackURI,nextAlbumArtURI,nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,Shuffle,Repeat,CrossfadeMode,Balance,HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,InfoSummarize2,InfoSummarize3,InfoSummarize4). Gibt an, welche Variable in das Reading <code>state</code> kopiert werden soll.</li> +</ul></li> +<li><b>Steueroptionen</b><ul> +<li><a name="SONOSPLAYER_attribut_maxVolume"><code>attr <name> maxVolume <int></code> +</a><br /> One of (0..100). Definiert die maximale Lautstärke dieses Zoneplayer.</li> +<li><a name="SONOSPLAYER_attribut_minVolume"><code>attr <name> minVolume <int></code> +</a><br /> One of (0..100). Definiert die minimale Lautstärke dieses Zoneplayer.</li> +<li><a name="SONOSPLAYER_attribut_maxVolumeHeadphone"><code>attr <name> maxVolumeHeadphone <int></code> +</a><br /> One of (0..100). Definiert die maximale Lautstärke dieses Zoneplayer im Kopfhörerbetrieb.</li> +<li><a name="SONOSPLAYER_attribut_minVolumeHeadphone"><code>attr <name> minVolumeHeadphone <int></code> +</a><br /> One of (0..100). Definiert die minimale Lautstärke dieses Zoneplayer im Kopfhörerbetrieb.</li> +<li><a name="SONOSPLAYER_attribut_buttonEvents"><code>attr <name> buttonEvents <Time:Pattern>[ <Time:Pattern> ...]</code> +</a><br /> Definiert, dass bei einer bestimten Tastenfolge am Player ein Event erzeugt werden soll. Die Definition der Events erfolgt als Tupel: Der erste Teil vor dem Doppelpunkt ist die Zeit in Sekunden, die berücksichtigt werden soll, der zweite Teil hinter dem Doppelpunkt definiert die Abfolge der Buttons, die für dieses Event notwendig sind.<br /> +Folgende Button-Kürzel sind zulässig: <ul><li><b>M</b>: Der Mute-Button</li><li><b>H</b>: Die Headphone-Buchse</li><li><b>U</b>: Up-Button (Lautstärke Hoch)</li><li><b>D</b>: Down-Button (Lautstärke Runter)</li></ul><br /> +Das Event, das geworfen wird, heißt <code>ButtonEvent</code>, der Wert ist die definierte Tastenfolge<br /> +Z.B.: <code>2:MM</code><br /> +Hier wird definiert, dass ein Event erzeugt werden soll, wenn innerhalb von 2 Sekunden zweimal die Mute-Taste gedrückt wurde. Das damit erzeugte Event hat dann den Namen <code>ButtonEvent</code>, und den Wert <code>MM</code>.</li> +</ul></li> +</ul> +<br /> +<a name="SONOSPLAYERexamples"></a> +<h4>Beispiele / Hinweise</h4> +<ul> +<li><a name="SONOSPLAYER_examples_InfoSummarize">Format von InfoSummarize:</a><br /> +<code>infoSummarizeX := <NormalAudio>:summarizeElem:</NormalAudio> <StreamAudio>:summarizeElem:</StreamAudio>|:summarizeElem:</code><br /> +<code>:summarizeElem: := <:variable:[ prefix=":text:"][ suffix=":text:"][ instead=":text:"][ ifempty=":text:"]/[ emptyVal=":text:"]></code><br /> +<code>:variable: := TransportState|NumberOfTracks|Track|TrackURI|TrackDuration|Title|Artist|Album|OriginalTrackNumber|AlbumArtist|Sender|SenderCurrent|SenderInfo|StreamAudio|NormalAudio|AlbumArtURI|nextTrackDuration|nextTrackURI|nextAlbumArtURI|nextTitle|nextArtist|nextAlbum|nextAlbumArtist|nextOriginalTrackNumber|Volume|Mute|Shuffle|Repeat|CrossfadeMode|Balance|HeadphoneConnected|SleepTimer|Presence|RoomName|SaveRoomName|PlayerType|Location|SoftwareRevision|SerialNum|InfoSummarize1|InfoSummarize2|InfoSummarize3|InfoSummarize4</code><br /> +<code>:text: := [Jeder beliebige Text ohne doppelte Anführungszeichen]</code><br /></li> +</ul> + +=end html_DE +=cut \ No newline at end of file diff --git a/fhem/FHEM/lib/Encode/transliterate_win1251.pm b/fhem/FHEM/lib/Encode/transliterate_win1251.pm new file mode 100644 index 000000000..82ff0af08 --- /dev/null +++ b/fhem/FHEM/lib/Encode/transliterate_win1251.pm @@ -0,0 +1,55 @@ +#!/usr/bin/perl -w +$VERSION = '1.00'; +use strict; +package Encode::transliterate_win1251; + +my $debug; + +# Assume that FROM are 1-char, and have no REx charclass special characters +my $uc = "ß ÂÅÐÒÛÓÈÎÏØ Ù ÀÑÄÔÃÕÉÊËÇÜ ÖÆ ÁÍÌÝÞ × ¨Ú"; +my $ul = "YAVERTYUIOPSHSCHASDFGHJKLZ''CZHBNMEYUCHE'"; + +# titlecase and random alternative translations +my $tc = "ß Ø Ù Æ Þ Þ Þ þ × × × ÷ ß ß ÿ ß ß ÿ Ù Ù ù ¨ ¨ ¸ "; +my $tl = "YaShSchZhYuIUIuiuChTchTCHtchIAIaiaJAJajaTCHTchtchJOJojo"; + +# Assume that 1-char parts of TO have no REx charclass special characters + +my $lc = "ÿ âåðòûóèîïø ù àñäôãõéêëçüöæ áíìýþ ÷ ¸ú¹"; +my $ll = "yavertyuiopshschasdfghjklz'czhbnmeyuche'N"; + +sub prepare_translation { + my ($from, $to) = @_; + die "Mismatch of length:\nfrom: '$from'\nto: '$to'\n" unless length($from) == length $to; + my @from = ($from =~ /(\S\s*)/g); + my (%hash_from, %hash_to); + for my $chunk (@from) { + my $chunk_to = substr($to, 0, length $chunk); + substr($to, 0, length $chunk) = ""; + $chunk =~ s/\s+$//; + $hash_from{$chunk} = $chunk_to; + # Prefer earlier definition for reverse translation + $hash_to{$chunk_to} = $chunk unless exists $hash_to{$chunk_to}; + } + (\%hash_from, \%hash_to) +} + +sub make_translator { + my ($hash) = @_; + die unless keys %$hash; + my @keys2 = grep length > 1, keys %$hash; + my $keys1 = join '', grep length == 1, keys %$hash; + my $rex = ''; + $rex .= (join('|', sort {length $b <=> length $a} @keys2) . '|') + if @keys2; + $rex .= "[\Q$keys1\E]" if length $keys1; + warn "rex = '$rex'\n" if $debug; + eval "sub {s/($rex)/\$hash->{\$1}/g}" or die; +} + +sub cyr_table {"$uc$lc$tc"} +sub lat_table {"$ul$ll$tl"} + +#my $to = make_translator( (prepare_translation("$uc$lc$tc", "$ul$ll$tl"))[0] ); + +1; diff --git a/fhem/FHEM/lib/MP3/Info.pm b/fhem/FHEM/lib/MP3/Info.pm new file mode 100644 index 000000000..9775b3849 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Info.pm @@ -0,0 +1,2939 @@ +package MP3::Info; + +# JRF: Added support for ID3v2.4 spec-valid frame size processing (falling back to old +# non-spec valid frame size processing) +# Added support for ID3v2.4 footers. +# Updated text frames to correct mis-terminated frame content. +# Added ignoring of encrypted frames. +# TODO: sort out flags for compression / DLI + +require 5.006; + +use strict; +use overload; +use Carp; +use Fcntl qw(:seek); + +use vars qw( + @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $REVISION + @mp3_genres %mp3_genres @winamp_genres %winamp_genres $try_harder + @t_bitrate @t_sampling_freq @frequency_tbl %v1_tag_fields + @v1_tag_names %v2_tag_names %v2_to_v1_names $AUTOLOAD + @mp3_info_fields %rva2_channel_types + $debug_24 $debug_Tencoding +); + +@ISA = 'Exporter'; +@EXPORT = qw( + set_mp3tag get_mp3tag get_mp3info remove_mp3tag + use_winamp_genres +); +@EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8); +%EXPORT_TAGS = ( + genres => [qw(@mp3_genres %mp3_genres)], + utf8 => [qw(use_mp3_utf8)], + all => [@EXPORT, @EXPORT_OK] +); + +# $Id: Info.pm 28 2008-11-09 01:08:44Z dsully $ +($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/; +$VERSION = '1.24'; + +# JRF: Whether we're debugging the ID3v2.4 support +$debug_24 = 0; +$debug_Tencoding = 0; + +=pod + +=head1 NAME + +MP3::Info - Manipulate / fetch info from MP3 audio files + +=head1 SYNOPSIS + + #!perl -w + use MP3::Info; + my $file = 'Pearls_Before_Swine.mp3'; + set_mp3tag($file, 'Pearls Before Swine', q"77's", + 'Sticks and Stones', '1990', + q"(c) 1990 77's LTD.", 'rock & roll'); + + my $tag = get_mp3tag($file) or die "No TAG info"; + $tag->{GENRE} = 'rock'; + set_mp3tag($file, $tag); + + my $info = get_mp3info($file); + printf "$file length is %d:%d\n", $info->{MM}, $info->{SS}; + +=cut + +{ + my $c = -1; + # set all lower-case and regular-cased versions of genres as keys + # with index as value of each key + %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres; + + # do it again for winamp genres + $c = -1; + %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres; +} + +=pod + + my $mp3 = new MP3::Info $file; + $mp3->title('Perls Before Swine'); + printf "$file length is %s, title is %s\n", + $mp3->time, $mp3->title; + + +=head1 DESCRIPTION + +=over 4 + +=item $mp3 = MP3::Info-E<gt>new(FILE) + +OOP interface to the rest of the module. The same keys +available via get_mp3info and get_mp3tag are available +via the returned object (using upper case or lower case; +but note that all-caps "VERSION" will return the module +version, not the MP3 version). + +Passing a value to one of the methods will set the value +for that tag in the MP3 file, if applicable. + +=cut + +sub new { + my($pack, $file) = @_; + + my $info = get_mp3info($file) or return undef; + my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names }; + my %self = ( + FILE => $file, + TRY_HARDER => 0 + ); + + @self{@mp3_info_fields, @v1_tag_names, 'file'} = ( + @{$info}{@mp3_info_fields}, + @{$tags}{@v1_tag_names}, + $file + ); + + return bless \%self, $pack; +} + +sub can { + my $self = shift; + return $self->SUPER::can(@_) unless ref $self; + my $name = uc shift; + return sub { $self->$name(@_) } if exists $self->{$name}; + return undef; +} + +sub AUTOLOAD { + my($self) = @_; + (my $name = uc $AUTOLOAD) =~ s/^.*://; + + if (exists $self->{$name}) { + my $sub = exists $v1_tag_fields{$name} + ? sub { + if (defined $_[1]) { + $_[0]->{$name} = $_[1]; + set_mp3tag($_[0]->{FILE}, $_[0]); + } + return $_[0]->{$name}; + } + : sub { + return $_[0]->{$name} + }; + + no strict 'refs'; + *{$AUTOLOAD} = $sub; + goto &$AUTOLOAD; + + } else { + carp(sprintf "No method '$name' available in package %s.", + __PACKAGE__); + } +} + +sub DESTROY { + +} + + +=item use_mp3_utf8([STATUS]) + +Tells MP3::Info to (or not) return TAG info in UTF-8. +TRUE is 1, FALSE is 0. Default is TRUE, if available. + +Will only be able to turn it on if Encode is available. ID3v2 +tags will be converted to UTF-8 according to the encoding specified +in each tag; ID3v1 tags will be assumed Latin-1 and converted +to UTF-8. + +Function returns status (TRUE/FALSE). If no argument is supplied, +or an unaccepted argument is supplied, function merely returns status. + +This function is not exported by default, but may be exported +with the C<:utf8> or C<:all> export tag. + +=cut + +my $unicode_base_module = eval { require Encode; require Encode::Guess }; + +my $UNICODE = use_mp3_utf8($unicode_base_module ? 1 : 0); + +eval { require Encode::Detect::Detector }; + +my $unicode_detect_module = $@ ? 0 : 1; + +sub use_mp3_utf8 { + my $val = shift; + + $UNICODE = 0; + + if ($val == 1) { + + if ($unicode_base_module) { + + $Encode::Guess::NoUTFAutoGuess = 1; + $UNICODE = 1; + } + } + + return $UNICODE; +} + +=pod + +=item use_winamp_genres() + +Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres> +(adds 68 additional genres to the default list of 80). +This is a separate function because these are non-standard +genres, but they are included because they are widely used. + +You can import the data structures with one of: + + use MP3::Info qw(:genres); + use MP3::Info qw(:DEFAULT :genres); + use MP3::Info qw(:all); + +=cut + +sub use_winamp_genres { + %mp3_genres = %winamp_genres; + @mp3_genres = @winamp_genres; + return 1; +} + +=pod + +=item remove_mp3tag (FILE [, VERSION, BUFFER]) + +Can remove ID3v1 or ID3v2 tags. VERSION should be C<1> for ID3v1 +(the default), C<2> for ID3v2, and C<ALL> for both. + +For ID3v1, removes last 128 bytes from file if those last 128 bytes begin +with the text 'TAG'. File will be 128 bytes shorter. + +For ID3v2, removes ID3v2 tag. Because an ID3v2 tag is at the +beginning of the file, we rewrite the file after removing the tag data. +The buffer for rewriting the file is 4MB. BUFFER (in bytes) ca +change the buffer size. + +Returns the number of bytes removed, or -1 if no tag removed, +or undef if there is an error. + +=cut + +sub remove_mp3tag { + my($file, $version, $buf) = @_; + my($fh, $return); + + $buf ||= 4096*1024; # the bigger the faster + $version ||= 1; + + if (not (defined $file && $file ne '')) { + $@ = "No file specified"; + return undef; + } + + if (not -s $file) { + $@ = "File is empty"; + return undef; + } + + if (ref $file) { # filehandle passed + $fh = $file; + } else { + if (not open $fh, '+<', $file) { + $@ = "Can't open $file: $!"; + return undef; + } + } + + binmode $fh; + + if ($version eq 1 || $version eq 'ALL') { + seek $fh, -128, SEEK_END; + my $tell = tell $fh; + if (<$fh> =~ /^TAG/) { + truncate $fh, $tell or carp "Can't truncate '$file': $!"; + $return += 128; + } + } + + if ($version eq 2 || $version eq 'ALL') { + my $v2h = _get_v2head($fh); + if ($v2h) { + local $\; + seek $fh, 0, SEEK_END; + my $eof = tell $fh; + my $off = $v2h->{tag_size}; + + while ($off < $eof) { + seek $fh, $off, SEEK_SET; + read $fh, my($bytes), $buf; + seek $fh, $off - $v2h->{tag_size}, SEEK_SET; + print $fh $bytes; + $off += $buf; + } + + truncate $fh, $eof - $v2h->{tag_size} + or carp "Can't truncate '$file': $!"; + $return += $v2h->{tag_size}; + } + + # JRF: I've not written the code to strip ID3v2.4 footers. + # Sorry, I'm lazy. + } + + _close($file, $fh); + + return $return || -1; +} + + +=pod + +=item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) + +=item set_mp3tag (FILE, $HASHREF) + +Adds/changes tag information in an MP3 audio file. Will clobber +any existing information in file. + +Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE. All fields have +a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE, +which is one byte in the file. The GENRE passed in the function is a +case-insensitive text string representing a genre found in C<@mp3_genres>. + +Will accept either a list of values, or a hashref of the type +returned by C<get_mp3tag>. + +If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be +28 bytes. + +ID3v2 support may come eventually. Note that if you set a tag on a file +with ID3v2, the set tag will be for ID3v1[.1] only, and if you call +C<get_mp3tag> on the file, it will show you the (unchanged) ID3v2 tags, +unless you specify ID3v1. + +=cut + +sub set_mp3tag { + my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_; + my(%info, $oldfh, $ref, $fh); + local %v1_tag_fields = %v1_tag_fields; + + # set each to '' if undef + for ($title, $artist, $album, $year, $comment, $tracknum, $genre, + (@info{@v1_tag_names})) + {$_ = defined() ? $_ : ''} + + ($ref) = (overload::StrVal($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/) + if ref $title; + # populate data to hashref if hashref is not passed + if (!$ref) { + (@info{@v1_tag_names}) = + ($title, $artist, $album, $year, $comment, $tracknum, $genre); + + # put data from hashref into hashref if hashref is passed + } elsif ($ref eq 'HASH') { + %info = %$title; + + # return otherwise + } else { + carp(<<'EOT'); +Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) + set_mp3tag (FILE, $HASHREF) +EOT + return undef; + } + + if (not (defined $file && $file ne '')) { + $@ = "No file specified"; + return undef; + } + + if (not -s $file) { + $@ = "File is empty"; + return undef; + } + + # comment field length 28 if ID3v1.1 + $v1_tag_fields{COMMENT} = 28 if $info{TRACKNUM}; + + + # only if -w is on + if ($^W) { + # warn if fields too long + foreach my $field (keys %v1_tag_fields) { + $info{$field} = '' unless defined $info{$field}; + if (length($info{$field}) > $v1_tag_fields{$field}) { + carp "Data too long for field $field: truncated to " . + "$v1_tag_fields{$field}"; + } + } + + if ($info{GENRE}) { + carp "Genre `$info{GENRE}' does not exist\n" + unless exists $mp3_genres{$info{GENRE}}; + } + } + + if ($info{TRACKNUM}) { + $info{TRACKNUM} =~ s/^(\d+)\/(\d+)$/$1/; + unless ($info{TRACKNUM} =~ /^\d+$/ && + $info{TRACKNUM} > 0 && $info{TRACKNUM} < 256) { + carp "Tracknum `$info{TRACKNUM}' must be an integer " . + "from 1 and 255\n" if $^W; + $info{TRACKNUM} = ''; + } + } + + if (ref $file) { # filehandle passed + $fh = $file; + } else { + if (not open $fh, '+<', $file) { + $@ = "Can't open $file: $!"; + return undef; + } + } + + binmode $fh; + $oldfh = select $fh; + seek $fh, -128, SEEK_END; + # go to end of file if no ID3v1 tag, beginning of existing tag if tag present + seek $fh, (<$fh> =~ /^TAG/ ? -128 : 0), SEEK_END; + + # get genre value + $info{GENRE} = $info{GENRE} && exists $mp3_genres{$info{GENRE}} ? + $mp3_genres{$info{GENRE}} : 255; # some default genre + + local $\; + # print TAG to file + if ($info{TRACKNUM}) { + print pack 'a3a30a30a30a4a28xCC', 'TAG', @info{@v1_tag_names}; + } else { + print pack 'a3a30a30a30a4a30C', 'TAG', @info{@v1_tag_names[0..4, 6]}; + } + + select $oldfh; + + _close($file, $fh); + + return 1; +} + +=pod + +=item get_mp3tag (FILE [, VERSION, RAW_V2, APE2]) + +Returns hash reference containing tag information in MP3 file. The keys +returned are the same as those supplied for C<set_mp3tag>, except in the +case of RAW_V2 being set. + +If VERSION is C<1>, the information is taken from the ID3v1 tag (if present). +If VERSION is C<2>, the information is taken from the ID3v2 tag (if present). +If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and +then, if present, the ID3v2 tag information will override any existing ID3v1 +tag info. + +If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation +of text encoding. The key name is the same as the frame ID (ID to name mappings +are in the global %v2_tag_names). + +If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if +necessary, etc. It also takes multiple values for a given key (such as comments) +and puts them in an arrayref. + +If APE is C<1>, an APE tag will be located before all other tags. + +If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will +not be read. + +Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>), +(unless RAW_V2 is C<1>). + +Also returns a TAGVERSION key, containing the ID3 version used for the returned +data (if TAGVERSION argument is C<0>, may contain two versions). + +=cut + +sub get_mp3tag { + my $file = shift; + my $ver = shift || 0; + my $raw = shift || 0; + my $find_ape = shift || 0; + my $fh; + + my $has_v1 = 0; + my $has_v2 = 0; + my $has_ape = 0; + my %info = (); + + # See if a version number was passed. Make sure it's a 1 or a 2 + $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0; + + if (!(defined $file && $file ne '')) { + $@ = "No file specified"; + return undef; + } + + my $filesize = -s $file; + + if (!$filesize) { + $@ = "File is empty"; + return undef; + } + + # filehandle passed + if (ref $file) { + + $fh = $file; + + } else { + + open($fh, $file) || do { + $@ = "Can't open $file: $!"; + return undef; + }; + } + + binmode $fh; + + # Try and find an APE Tag - this is where FooBar2k & others + # store ReplayGain information + if ($find_ape) { + + $has_ape = _parse_ape_tag($fh, $filesize, \%info); + } + + if ($ver < 2) { + + $has_v1 = _get_v1tag($fh, \%info); + + if ($ver == 1 && !$has_v1) { + _close($file, $fh); + $@ = "No ID3v1 tag found"; + return undef; + } + } + + if ($ver == 2 || $ver == 0) { + $has_v2 = _get_v2tag($fh, $ver, $raw, \%info); + } + + if (!$has_v1 && !$has_v2 && !$has_ape) { + _close($file, $fh); + $@ = "No ID3 or APE tag found"; + return undef; + } + + unless ($raw && $ver == 2) { + + # Strip out NULLs unless we want the raw data. + foreach my $key (keys %info) { + + if (defined $info{$key}) { + $info{$key} =~ s/\000+.*//g; + $info{$key} =~ s/\s+$//; + } + } + + for (@v1_tag_names) { + $info{$_} = '' unless defined $info{$_}; + } + } + + if (keys %info && !defined $info{'GENRE'}) { + $info{'GENRE'} = ''; + } + + _close($file, $fh); + + return keys %info ? \%info : undef; +} + +sub _get_v1tag { + my ($fh, $info) = @_; + + seek $fh, -128, SEEK_END; + read($fh, my $tag, 128); + + if (!defined($tag) || $tag !~ /^TAG/) { + + return 0; + } + + if (substr($tag, -3, 2) =~ /\000[^\000]/) { + + (undef, @{$info}{@v1_tag_names}) = + (unpack('a3a30a30a30a4a28', $tag), + ord(substr($tag, -2, 1)), + $mp3_genres[ord(substr $tag, -1)]); + + $info->{'TAGVERSION'} = 'ID3v1.1'; + + } else { + + (undef, @{$info}{@v1_tag_names[0..4, 6]}) = + (unpack('a3a30a30a30a4a30', $tag), + $mp3_genres[ord(substr $tag, -1)]); + + $info->{'TAGVERSION'} = 'ID3v1'; + } + + if (!$UNICODE) { + return 1; + } + + # Save off the old suspects list, since we add + # iso-8859-1 below, but don't want that there + # for possible ID3 v2.x parsing below. + my $oldSuspects = $Encode::Encoding{'Guess'}->{'Suspects'}; + + for my $key (keys %{$info}) { + + next unless $info->{$key}; + + # Try and guess the encoding. + if ($unicode_detect_module) { + + my $charset = Encode::Detect::Detector::detect($info->{$key}) || 'iso-8859-1'; + my $enc = Encode::find_encoding($charset); + + if ($enc) { + + $info->{$key} = $enc->decode($info->{$key}, 0); + + next; + } + } + + my $value = $info->{$key}; + my $icode = Encode::Guess->guess($value); + + if (!ref($icode)) { + + # Often Latin1 bytes are + # stuffed into a 1.1 tag. + Encode::Guess->add_suspects('iso-8859-1'); + + while (length($value)) { + + $icode = Encode::Guess->guess($value); + + last if ref($icode); + + # Remove garbage and retry + # (string is truncated in the + # middle of a multibyte char?) + $value =~ s/(.)$//; + } + } + + $info->{$key} = Encode::decode(ref($icode) ? $icode->name : 'iso-8859-1', $info->{$key}); + + # Trim any trailing nuls + $info->{$key} =~ s/\x00+$//g; + } + + Encode::Guess->set_suspects(keys %{$oldSuspects}); + + return 1; +} + +sub _parse_v2tag { + my ($ver, $raw_v2, $v2, $info) = @_; + + # Make sure any existing TXXX flags are an array. + # As we might need to append comments to it below. + if ($v2->{'TXXX'} && ref($v2->{'TXXX'}) ne 'ARRAY') { + + $v2->{'TXXX'} = [ $v2->{'TXXX'} ]; + } + + # J.River Media Center sticks RG tags in comments. + # Ugh. Make them look like TXXX tags, which is really what they are. + if (ref($v2->{'COMM'}) eq 'ARRAY' && grep { /Media Jukebox/ } @{$v2->{'COMM'}}) { + + for my $comment (@{$v2->{'COMM'}}) { + + if ($comment =~ /Media Jukebox/) { + + # we only want one null to lead. + $comment =~ s/^\000+//g; + + push @{$v2->{'TXXX'}}, "\000$comment"; + } + } + } + + my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names; + + for my $id (keys %{$hash}) { + + next if !exists $v2->{$id}; + + if ($id =~ /^UFID?$/) { + + my @ufid_list = split(/\0/, $v2->{$id}); + + $info->{$hash->{$id}} = $ufid_list[1] if ($#ufid_list > 0); + + } elsif ($id =~ /^RVA[D2]?$/) { + + # Expand these binary fields. See the ID3 spec for Relative Volume Adjustment. + if ($id eq 'RVA2') { + + # ID is a text string + ($info->{$hash->{$id}}->{'ID'}, my $rvad) = split /\0/, $v2->{$id}; + + my $channel = $rva2_channel_types{ ord(substr($rvad, 0, 1, '')) }; + + $info->{$hash->{$id}}->{$channel}->{'REPLAYGAIN_TRACK_GAIN'} = + sprintf('%f', _grab_int_16(\$rvad) / 512); + + my $peakBytes = ord(substr($rvad, 0, 1, '')); + + if (int($peakBytes / 8)) { + + $info->{$hash->{$id}}->{$channel}->{'REPLAYGAIN_TRACK_PEAK'} = + sprintf('%f', _grab_int_16(\$rvad) / 512); + } + + } elsif ($id eq 'RVAD' || $id eq 'RVA') { + + my $rvad = $v2->{$id}; + my $flags = ord(substr($rvad, 0, 1, '')); + my $desc = ord(substr($rvad, 0, 1, '')); + + # iTunes appears to be the only program that actually writes + # out a RVA/RVAD tag. Everyone else punts. + for my $type (qw(REPLAYGAIN_TRACK_GAIN REPLAYGAIN_TRACK_PEAK)) { + + for my $channel (qw(RIGHT LEFT)) { + + my $val = _grab_uint_16(\$rvad) / 256; + + # iTunes uses a range of -255 to 255 + # to be -100% (silent) to 100% (+6dB) + if ($val == -255) { + $val = -96.0; + } else { + $val = 20.0 * log(($val+255)/255)/log(10); + } + + $info->{$hash->{$id}}->{$channel}->{$type} = $flags & 0x01 ? $val : -$val; + } + } + } + + } elsif ($id =~ /^A?PIC$/) { + + my $pic = $v2->{$id}; + + # if there is more than one picture, just grab the first one. + # JRF: Should consider looking for either the thumbnail or the front cover, + # rather than just returning the first one. + # Possibly also checking that the format is actually understood, + # but that's really down to the caller - we can't say whether the + # format is understood here. + if (ref($pic) eq 'ARRAY') { + $pic = (@$pic)[0]; + } + + use bytes; + + my $valid_pic = 0; + my $pic_len = 0; + my $pic_format = ''; + + # look for ID3 v2.2 picture + if ($pic && $id eq 'PIC') { + + # look for ID3 v2.2 picture + my ($encoding, $format, $picture_type, $description) = unpack 'Ca3CZ*', $pic; + $pic_len = length($description) + 1 + 5; + + # skip extra terminating null if unicode + if ($encoding) { $pic_len++; } + + if ($pic_len < length($pic)) { + $valid_pic = 1; + $pic_format = $format; + } + + } elsif ($pic && $id eq 'APIC') { + + # look for ID3 v2.3/2.4 picture + my ($encoding, $format) = unpack 'C Z*', $pic; + + $pic_len = length($format) + 2; + + if ($pic_len < length($pic)) { + + my ($picture_type, $description) = unpack "x$pic_len C Z*", $pic; + + $pic_len += 1 + length($description) + 1; + + # skip extra terminating null if UTF-16 (encoding 1 or 2) + if ( $encoding == 1 || $encoding == 2 ) { $pic_len++; } + + $valid_pic = 1; + $pic_format = $format; + } + } + + # Proceed if we have a valid picture. + if ($valid_pic && $pic_format) { + + my ($data) = unpack("x$pic_len A*", $pic); + + if (length($data) && $pic_format) { + + $info->{$hash->{$id}} = { + 'DATA' => $data, + 'FORMAT' => $pic_format, + } + } + } + + } else { + my $data1 = $v2->{$id}; + + $data1 = [ $data1 ] if ref($data1) ne 'ARRAY'; + + for my $data (@$data1) { + # TODO : this should only be done for certain frames; + # using RAW still gives you access, but we should be smarter + # about how individual frame types are handled. it's not + # like the list is infinitely long. + $data =~ s/^(.)//; # strip first char (text encoding) + my $encoding = $1; + my $desc; + + # Comments & Unsyncronized Lyrics have the same format. + if ($id =~ /^(COM[M ]?|US?LT)$/) { # space for iTunes brokenness + + $data =~ s/^(?:...)//; # strip language + } + + # JRF: I believe this should probably only be applied to the text frames + # and not every single frame. + if ($UNICODE) { + + if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE + # text fields can be null-separated lists; + # UTF-16 therefore needs special care + # + # foobar2000 encodes tags in UTF-16LE + # (which is apparently illegal) + # Encode dies on a bad BOM, so it is + # probably wise to wrap it in an eval + # anyway + $data = eval { Encode::decode('utf16', $data) } || Encode::decode('utf16le', $data); + + } elsif ($encoding eq "\003") { # UTF-8 + + # make sure string is UTF8, and set flag appropriately + $data = Encode::decode('utf8', $data); + + } elsif ($encoding eq "\000") { + + # Only guess if it's not ascii. + if ($data && $data !~ /^[\x00-\x7F]+$/) { + + if ($unicode_detect_module) { + + my $charset = Encode::Detect::Detector::detect($data) || 'iso-8859-1'; + my $enc = Encode::find_encoding($charset); + + if ($enc) { + $data = $enc->decode($data, 0); + } + + } else { + + # Try and guess the encoding, otherwise just use latin1 + my $dec = Encode::Guess->guess($data); + + if (ref $dec) { + $data = $dec->decode($data); + } else { + # Best try + $data = Encode::decode('iso-8859-1', $data); + } + } + } + } + + } else { + + # If the string starts with an + # UTF-16 little endian BOM, use a hack to + # convert to ASCII per best-effort + my $pat; + if ($data =~ s/^\xFF\xFE//) { + # strip additional BOMs as seen in COM(M?) and TXX(X?) + $data = join ("",map { ( /^(..)$/ && ! /(\xFF\xFE)/ )? $_: "" } (split /(..)/, $data)); + $pat = 'v'; + } elsif ($data =~ s/^\xFE\xFF//) { + # strip additional BOMs as seen in COM(M?) and TXX(X?) + $data = join ("",map { ( /^(..)$/ && ! /(\xFF\xFE)/ )? $_: "" } (split /(..)/, $data)); + $pat = 'n'; + } + + if ($pat) { + # strip additional 0s + $data = join ("",map { ( /^(..)$/ && ! /(\x00\x00)/ )? $_: "" } (split /(..)/, $data)); + $data = pack 'C*', map { + (chr =~ /[[:ascii:]]/ && chr =~ /[[:print:]]/) + ? $_ + : ord('?') + } unpack "$pat*", $data; + } + } + + # We do this after decoding so we could be certain we're dealing + # with 8-bit text. + if ($id =~ /^(COM[M ]?|US?LT)$/) { # space for iTunes brokenness + + $data =~ s/^(.*?)\000//; # strip up to first NULL(s), + # for sub-comments (TODO: + # handle all comment data) + $desc = $1; + + if ($encoding eq "\001" || $encoding eq "\002") { + + $data =~ s/^\x{feff}//; + } + + } elsif ($id =~ /^TCON?$/) { + + my ($index, $name); + + # Turn multiple nulls into a single. + $data =~ s/\000+/\000/g; + + # Handle the ID3v2.x spec - + # + # just an index number, possibly + # paren enclosed - referer to the v1 genres. + if ($data =~ /^ \(? (\d+) \)?\000?$/sx) { + + $index = $1; + + # Paren enclosed index with refinement. + # (4)Eurodisco + } elsif ($data =~ /^ \( (\d+) \)\000? ([^\(].+)$/x) { + + ($index, $name) = ($1, $2); + + # List of indexes: (37)(38) + } elsif ($data =~ /^ \( (\d+) \)\000?/x) { + + my @genres = (); + + while ($data =~ s/^ \( (\d+) \)//x) { + + # The indexes might have a refinement + # not sure why one wouldn't just use + # the proper genre in the first place.. + if ($data =~ s/^ ( [^\(]\D+ ) ( \000 | \( | \Z)/$2/x) { + + push @genres, $1; + + } else { + + push @genres, $mp3_genres[$1]; + } + } + + $data = \@genres; + + } elsif ($data =~ /^[^\000]+\000/) { + + # name genres separated by nulls. + $data = [ split /\000/, $data ]; + } + + # Text based genres will fall through. + if ($name && $name ne "\000") { + $data = $name; + } elsif (defined $index) { + $data = $mp3_genres[$index]; + } + + # Collapse single genres down, as we may have another tag. + if ($data && ref($data) eq 'ARRAY' && scalar @$data == 1) { + + $data = $data->[0]; + } + + } elsif ($id =~ /^T...?$/ && $id ne 'TXXX') { + + # In ID3v2.4 there's a slight content change for text fields. + # They can contain multiple values which are nul terminated + # within the frame. We ONLY want to split these into multiple + # array values if they didn't request raw values (1). + # raw_v2 = 0 => parse simply + # raw_v2 = 1 => don't parse + # raw_v2 = 2 => do split into arrayrefs + + # Strip off any trailing NULs, which would indicate an empty + # field and cause an array with no elements to be created. + $data =~ s/\x00+$//; + + + if ($data =~ /\x00/ && ($raw_v2 == 2 || $raw_v2 == 0)) + { + # There are embedded nuls in the string, which means an ID3v2.4 + # multi-value frame. And they wanted arrays rather than simple + # values. + # Strings are already UTF-8, so any double nuls from 16 bit + # characters will have already been reduced to single nuls. + $data = [ split /\000/, $data ]; + } + } + + if ($desc) + { + # It's a frame with a description, so we may need to construct a hash + # for the data, rather than an array. + if ($raw_v2 == 2) { + + $data = { $desc => $data }; + + } elsif ($desc =~ /^iTun/) { + + # leave iTunes tags alone. + $data = join(' ', $desc, $data); + } + } + + if ($raw_v2 == 2 && exists $info->{$hash->{$id}}) { + + if (ref $info->{$hash->{$id}} eq 'ARRAY') { + push @{$info->{$hash->{$id}}}, $data; + } else { + $info->{$hash->{$id}} = [ $info->{$hash->{$id}}, $data ]; + } + + } else { + + # User defined frame + if ($id eq 'TXXX') { + + my ($key, $val) = split(/\0/, $data); + + # Some programs - such as FB2K leave a UTF-16 BOM on the value + if ($encoding eq "\001" || $encoding eq "\002") { + + $val =~ s/^\x{feff}//; + } + + $info->{uc($key)} = $val; + + } elsif ($id eq 'PRIV') { + + my ($key, $val) = split(/\0/, $data); + $info->{uc($key)} = unpack('v', $val); + + } else { + + my $key = $hash->{$id}; + + # If we have multiple values + # for the same key - turn them + # into an array ref. + if ($ver == 2 && $info->{$key} && !ref($info->{$key})) { + + if (ref($data) eq "ARRAY") { + + $info->{$key} = [ $info->{$key}, @$data ]; + } else { + + my $old = delete $info->{$key}; + + @{$info->{$key}} = ($old, $data); + } + + } elsif ($ver == 2 && ref($info->{$key}) eq 'ARRAY') { + + if (ref($data) eq "ARRAY") { + + push @{$info->{$key}}, @$data; + + } else { + + push @{$info->{$key}}, $data; + } + + } else { + + $info->{$key} = $data; + } + } + } + } + } + } +} + +sub _get_v2tag { + my ($fh, $ver, $raw, $info, $start) = @_; + my $eof; + my $gotanyv2 = 0; + + # First we need to check the end of the file for any footer + + seek $fh, -128, SEEK_END; + $eof = (tell $fh) + 128; + + # go to end of file if no ID3v1 tag, beginning of existing tag if tag present + if (<$fh> =~ /^TAG/) { + $eof -= 128; + } + + seek $fh, $eof, SEEK_SET; + # print STDERR "Checking for footer at $eof\n"; + + if (my $v2f = _get_v2foot($fh)) { + $eof -= $v2f->{tag_size}; + # We have a ID3v2.4 footer. Must read it. + $gotanyv2 |= (_get_v2tagdata($fh, $ver, $raw, $info, $eof) ? 2 : 0); + } + + # Now read any ID3v2 header + $gotanyv2 |= (_get_v2tagdata($fh, $ver, $raw, $info, $start) ? 1 : 0); + + # Because we've merged the entries it makes sense to trim any duplicated + # values - for example if there's a footer and a header that contain the same + # data then this results in every entry being an array containing two + # identical values. + for my $name (keys %{$info}) + { + # Note: We must not sort these elements to do the comparison because that + # changes the order in which they are claimed to appear. Whilst this + # probably isn't important, it may matter for default display - for + # example a lyric should be shown by default with the first entry + # in the tag in the case where the user has not specified a language + # preference. If we sorted the array it would destroy that order. + # This is a longwinded way of checking for duplicates and only writing the + # first element - we check the array for duplicates and clear all subsequent + # entries which are duplicates of earlier ones. + if (ref $info->{$name} eq 'ARRAY') + { + my @array = (); + my ($i, $o); + my @chk = @{$info->{$name}}; + for $i ( 0..$#chk ) + { + my $ielement = $chk[$i]; + if (defined $ielement) + { + for $o ( ($i+1)..$#chk ) + { + $chk[$o] = undef if (defined $o && defined $chk[$o] && ($ielement eq $chk[$o])); + } + push @array, $ielement; + } + } + # We may have reduced the array to a single element. If so, just assign + # a regular scalar instead of the array. + if ($#array == 0) + { + $info->{$name} = $array[0]; + } + else + { + $info->{$name} = \@array; + } + } + } + + return $gotanyv2; +} + +# $has_v2 = &_get_v2tagdata($filehandle, $ver, $raw, $info, $startinfile); +# $info is a hash reference which will be updated with the new ID3v2 details +# if the updated bit is set, and set to the new details if the updated bit +# is clear. +# If undefined, $startinfile will be treated as 0 (see _get_v2head). +# $v2h is a reference to a hash of the frames present within the tag. +# Any frames which are repeated within the tag (eg USLT with different +# languages) will be supplied as an array rather than a scalar. All client +# code needs to be aware that any frame may be duplicated. +sub _get_v2tagdata { + my($fh, $ver, $raw, $info, $start) = @_; + my($off, $end, $myseek, $v2, $v2h, $hlen, $num, $wholetag); + + $v2 = {}; + $v2h = _get_v2head($fh, $start) or return 0; + + if ($v2h->{major_version} < 2) { + carp "This is $v2h->{version}; " . + "ID3v2 versions older than ID3v2.2.0 not supported\n" + if $^W; + return 0; + } + + # use syncsafe bytes if using version 2.4 + my $id3v2_4_frame_size_broken = 0; + my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256; + + # alas, that's what the spec says, but iTunes and others don't syncsafe + # the length, which breaks MP3 files with v2.4 tags longer than 128 bytes, + # like every image file. + # Because we should not break the spec conformant files due to + # spec-inconformant programs, we first try the correct form and if the + # data looks wrong we revert to broken behaviour. + + if ($v2h->{major_version} == 2) { + $hlen = 6; + $num = 3; + } else { + $hlen = 10; + $num = 4; + } + + $off = $v2h->{ext_header_size} + 10; + $end = $v2h->{tag_size} + 10; # should we read in the footer too? + + # JRF: If the format was ID3v2.2 and the compression bit was set, then we can't + # actually read the content because there are no defined compression schemes + # for ID3v2.2. Perform no more processing, and return failure because we + # cannot read anything. + return 0 if ($v2h->{major_version} == 2 && $v2h->{compression}); + + # JRF: If the update flag is set then the input data is the same as that which was + # passed in. ID3v2.4 section 3.2. + if ($v2h->{update}) { + $v2 = $info; + } + + # Bug 8939, Trying to read past the end of the file may crash on win32 + my $size = -s $fh; + if ( $v2h->{offset} + $end > $size ) { + $end -= $v2h->{offset} + $end - $size; + } + + seek $fh, $v2h->{offset}, SEEK_SET; + read $fh, $wholetag, $end; + + # JRF: The discrepency between ID3v2.3 and ID3v2.4 is that : + # 2.3: unsync flag indicates that unsync is used on the entire tag + # 2.4: unsync flag indicates that all frames have the unsync bit set + # In 2.4 this means that the size of the frames which have the unsync bit + # set will be the unsync'd size (section 4. in the ID3v2.4.0 structure + # specification). + # This means that when processing 2.4 files we should perform all the + # unsynchronisation processing at the frame level, not the tag level. + # The tag unsync bit is redundant (IMO). + if ($v2h->{major_version} == 4) { + $v2h->{unsync} = 0 + } + + $wholetag =~ s/\xFF\x00/\xFF/gs if $v2h->{unsync}; + + # JRF: If we /knew/ there would be something special in the tag which meant + # that the ID3v2.4 frame size was broken we could check it here. If, + # for example, the iTunes files had the word 'iTunes' somewhere in the + # tag and we knew that it was broken for versions below 3.145 (which is + # a number I just picked out of the air), then we could do something like this : + # if ($v2h->{major_version} == 4) && + # $wholetag =~ /iTunes ([0-9]+\.[0-9]+)/ && + # $1 < 3.145) + # { + # $id3v2_4_frame_size_broken = 1; + # } + # However I have not included this because I don't have examples of broken + # files - and in any case couldn't guarentee I'd get it right. + + $myseek = sub { + return unless $wholetag; + + my $bytes = substr($wholetag, $off, $hlen); + + # iTunes is stupid and sticks ID3v2.2 3 byte frames in a + # ID3v2.3 or 2.4 header. Ignore tags with a space in them. + if ($bytes !~ /^([A-Z0-9\? ]{$num})/) { + return; + } + + my ($id, $size) = ($1, $hlen); + my @bytes = reverse unpack "C$num", substr($bytes, $num, $num); + + for my $i (0 .. ($num - 1)) { + $size += $bytes[$i] * $bytesize ** $i; + } + + # JRF: Now provide the fall back for the broken ID3v2.4 frame size + # (which will persist for subsequent frames if detected). + + # Part 1: If the frame size cannot be valid according to the + # specification (or if it would be larger than the tag + # size allows). + if ($v2h->{major_version}==4 && + $id3v2_4_frame_size_broken == 0 && # we haven't detected brokenness yet + ((($bytes[0] | $bytes[1] | $bytes[2] | $bytes[3]) & 0x80) != 0 || # 0-bits set in size + $off + $size > $end) # frame size would excede the tag end + ) + { + # The frame is definately not correct for the specification, so drop to + # broken frame size system instead. + $bytesize = 128; + $size -= $hlen; # hlen has alread been added, so take that off again + $size = (($size & 0x0000007f)) | + (($size & 0x00003f80)<<1) | + (($size & 0x001fc000)<<2) | + (($size & 0x0fe00000)<<3); # convert spec to non-spec sizes + + $size += $hlen; # and re-add header len so that the entire frame's size is known + + $id3v2_4_frame_size_broken = 1; + + print "Frame size cannot be valid ID3v2.4 (part 1); reverting to broken behaviour\n" if ($debug_24); + + } + + # Part 2: If the frame size would result in the following frame being + # invalid. + if ($v2h->{major_version}==4 && + $id3v2_4_frame_size_broken == 0 && # we haven't detected brokenness yet + $size > 0x80+$hlen && # ignore frames that are too short to ever be wrong + $off + $size < $end) + { + + print "Frame size might not be valid ID3v2.4 (part 2); checking for following frame validity\n" if ($debug_24); + + my $morebytes = substr($wholetag, $off+$size, 4); + + if (! ($morebytes =~ /^([A-Z0-9]{4})/ || $morebytes =~ /^\x00{4}/) ) { + + # The next tag cannot be valid because its name is wrong, which means that + # either the size must be invalid or the next frame truely is broken. + # Either way, we can try to reduce the size to see. + my $retrysize; + + print " following frame isn't valid using spec\n" if ($debug_24); + + $retrysize = $size - $hlen; # remove already added header length + $retrysize = (($retrysize & 0x0000007f)) | + (($retrysize & 0x00003f80)<<1) | + (($retrysize & 0x001fc000)<<2) | + (($retrysize & 0x0fe00000)<<3); # convert spec to non-spec sizes + + $retrysize += $hlen; # and re-add header len so that the entire frame's size is known + + if (length($wholetag) >= ($off+$retrysize+4)) { + + $morebytes = substr($wholetag, $off+$retrysize, 4); + + } else { + + $morebytes = ''; + } + + if (! ($morebytes =~ /^([A-Z0-9]{4})/ || + $morebytes =~ /^\x00{4}/ || + $off + $retrysize > $end) ) + { + # With the retry at the smaller size, the following frame still isn't valid + # so the only thing we can assume is that this frame is just broken beyond + # repair. Give up right now - there's no way we can recover. + print " and isn't valid using broken-spec support; giving up\n" if ($debug_24); + return; + } + + print " but is fine with broken-spec support; reverting to broken behaviour\n" if ($debug_24); + + # We're happy that the non-spec size looks valid to lead us to the next frame. + # We might be wrong, generating false-positives, but that's really what you + # get for trying to handle applications that don't handle the spec properly - + # use something that isn't broken. + # (this is a copy of the recovery code in part 1) + $size = $retrysize; + $bytesize = 128; + $id3v2_4_frame_size_broken = 1; + + } else { + + print " looks like valid following frame; keeping spec behaviour\n" if ($debug_24); + + } + } + + my $flags = {}; + + # JRF: was > 3, but that's not true; future versions may be incompatible + if ($v2h->{major_version} == 4) { + my @bits = split //, unpack 'B16', substr($bytes, 8, 2); + $flags->{frame_zlib} = $bits[12]; # JRF: need to know about compressed + $flags->{frame_encrypt} = $bits[13]; # JRF: ... and encrypt + $flags->{frame_unsync} = $bits[14]; + $flags->{data_len_indicator} = $bits[15]; + } + + # JRF: version 3 was in a different order + elsif ($v2h->{major_version} == 3) { + my @bits = split //, unpack 'B16', substr($bytes, 8, 2); + $flags->{frame_zlib} = $bits[8]; # JRF: need to know about compressed + $flags->{data_len_indicator} = $bits[8]; # JRF: and compression implies the DLI is present + $flags->{frame_encrypt} = $bits[9]; # JRF: ... and encrypt + } + + return ($id, $size, $flags); + }; + + while ($off < $end) { + my ($id, $size, $flags) = &$myseek or last; + my ($hlenextra) = 0; + + # NOTE: Wrong; the encrypt comes after the DLI. maybe. + # JRF: Encrypted frames need to be decrypted first + if ($flags->{frame_encrypt}) { + + my ($encypt_method) = substr($wholetag, $off+$hlen+$hlenextra, 1); + + $hlenextra++; + + # We don't actually know how to decrypt anything, so we'll just skip the entire frame. + $off += $size; + + next; + } + + my $bytes = substr($wholetag, $off+$hlen+$hlenextra, $size-$hlen-$hlenextra); + + my $data_len; + if ($flags->{data_len_indicator}) { + $data_len = 0; + + my @data_len_bytes = reverse unpack 'C4', substr($bytes, 0, 4); + + $bytes = substr($bytes, 4); + + for my $i (0..3) { + $data_len += $data_len_bytes[$i] * 128 ** $i; + } + } + + print "got $id, length " . length($bytes) . " frameunsync: ".$flags->{frame_unsync}." tag unsync: ".$v2h->{unsync} ."\n" if ($debug_24); + + # perform frame-level unsync if needed (skip if already done for whole tag) + $bytes =~ s/\xFF\x00/\xFF/gs if $flags->{frame_unsync} && !$v2h->{unsync}; + + # JRF: Decompress now if compressed. + # (FIXME: Not implemented yet) + + # if we know the data length, sanity check it now. + if ($flags->{data_len_indicator} && defined $data_len) { + carp("Size mismatch on $id\n") unless $data_len == length($bytes); + } + + # JRF: Apply small sanity check on text elements - they must end with : + # a 0 if they are ISO8859-1 + # 0,0 if they are unicode + # (This is handy because it can be caught by the 'duplicate elements' + # in array checks) + # There is a question in my mind whether I should be doing this here - it + # is introducing knowledge of frame content format into the raw reader + # which is not a good idea. But if the frames are broken we at least + # recover. + if (($v2h->{major_version} == 3 || $v2h->{major_version} == 4) && $id =~ /^T/) { + + my $encoding = substr($bytes, 0, 1); + + # Both these cases are candidates for providing some warning, I feel. + # ISO-8859-1 or UTF-8 $bytes + if (($encoding eq "\x00" || $encoding eq "\x03") && $bytes !~ /\x00$/) { + + $bytes .= "\x00"; + print "Text frame $id has malformed ISO-8859-1/UTF-8 content\n" if ($debug_Tencoding); + + # # UTF-16, UTF-16BE + } elsif ( ($encoding eq "\x01" || $encoding eq "\x02") && $bytes !~ /\x00\x00$/) { + + $bytes .= "\x00\x00"; + print "Text frame $id has malformed UTF-16/UTF-16BE content\n" if ($debug_Tencoding); + + } else { + + # Other encodings cannot be fixed up (we don't know how 'cos they're not defined). + } + } + + if (exists $v2->{$id}) { + + if (ref $v2->{$id} eq 'ARRAY') { + push @{$v2->{$id}}, $bytes; + } else { + $v2->{$id} = [$v2->{$id}, $bytes]; + } + + } else { + + $v2->{$id} = $bytes; + } + + $off += $size; + } + + if (($ver == 0 || $ver == 2) && $v2) { + + if ($raw == 1 && $ver == 2) { + + %$info = %$v2; + + $info->{'TAGVERSION'} = $v2h->{'version'}; + + } else { + + _parse_v2tag($ver, $raw, $v2, $info); + + if ($ver == 0 && $info->{'TAGVERSION'}) { + $info->{'TAGVERSION'} .= ' / ' . $v2h->{'version'}; + } else { + $info->{'TAGVERSION'} = $v2h->{'version'}; + } + } + } + + return 1; +} + +=pod + +=item get_mp3info (FILE) + +Returns hash reference containing file information for MP3 file. +This data cannot be changed. Returned data: + + VERSION MPEG audio version (1, 2, 2.5) + LAYER MPEG layer description (1, 2, 3) + STEREO boolean for audio is in stereo + + VBR boolean for variable bitrate + BITRATE bitrate in kbps (average for VBR files) + FREQUENCY frequency in kHz + SIZE bytes in audio stream + OFFSET bytes offset that stream begins + + SECS total seconds + MM minutes + SS leftover seconds + MS leftover milliseconds + TIME time in MM:SS + + COPYRIGHT boolean for audio is copyrighted + PADDING boolean for MP3 frames are padded + MODE channel mode (0 = stereo, 1 = joint stereo, + 2 = dual channel, 3 = single channel) + FRAMES approximate number of frames + FRAME_LENGTH approximate length of a frame + VBR_SCALE VBR scale from VBR header + +On error, returns nothing and sets C<$@>. + +=cut + +sub get_mp3info { + my($file) = @_; + my($off, $byte, $eof, $h, $tot, $fh); + + if (not (defined $file && $file ne '')) { + $@ = "No file specified"; + return undef; + } + + my $size = -s $file; + + if (ref $file) { # filehandle passed + $fh = $file; + } else { + if ( !$size ) { + $@ = "File is empty"; + return undef; + } + + if (not open $fh, '<', $file) { + $@ = "Can't open $file: $!"; + return undef; + } + } + + $off = 0; + $tot = 8192; + + # Let the caller change how far we seek in looking for a header. + if ($try_harder) { + $tot *= $try_harder; + } + + binmode $fh; + seek $fh, $off, SEEK_SET; + read $fh, $byte, 4; + + if (my $v2h = _get_v2head($fh)) { + $tot += $off += $v2h->{tag_size}; + + if ( $off > $size - 10 ) { + # Invalid v2 tag size + $off = 0; + } + + seek $fh, $off, SEEK_SET; + read $fh, $byte, 4; + } + + $h = _get_head($byte); + my $is_mp3 = _is_mp3($h); + + # the head wasn't where we were expecting it.. dig deeper. + unless ($is_mp3) { + + # do only one read - it's _much_ faster + $off++; + seek $fh, $off, SEEK_SET; + read $fh, $byte, $tot; + + my $i; + + # now walk the bytes looking for the head + for ($i = 0; $i < $tot; $i++) { + + last if ($tot - $i) < 4; + + my $head = substr($byte, $i, 4) || last; + + next if (ord($head) != 0xff); + + $h = _get_head($head); + $is_mp3 = _is_mp3($h); + last if $is_mp3; + } + + # adjust where we are for _get_vbr() + $off += $i; + + if ($off > $tot && !$try_harder) { + _close($file, $fh); + $@ = "Couldn't find MP3 header (perhaps set " . + '$MP3::Info::try_harder and retry)'; + return undef; + } + } + + $h->{offset} = $off; + + my $vbr = _get_vbr($fh, $h, \$off); + my $lame = _get_lame($fh, $h, \$off); + + seek $fh, 0, SEEK_END; + $eof = tell $fh; + seek $fh, -128, SEEK_END; + $eof -= 128 if <$fh> =~ /^TAG/ ? 1 : 0; + + # JRF: Check for an ID3v2.4 footer and if present, remove it from + # the size. + seek($fh, $eof, SEEK_SET); + + if (my $v2f = _get_v2foot($fh)) { + $eof -= $v2f->{tag_size}; + } + + _close($file, $fh); + + $h->{size} = $eof - $off; + + return _get_info($h, $vbr, $lame); +} + +sub _get_info { + my($h, $vbr, $lame) = @_; + my $i; + + # No bitrate or sample rate? Something's wrong. + unless ($h->{bitrate} && $h->{fs}) { + return {}; + } + + $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 : $h->{IDR} == 0 ? 2.5 : 0; + $i->{LAYER} = 4 - $h->{layer}; + + if (ref($vbr) eq 'HASH' and $vbr->{is_vbr} == 1) { + $i->{VBR} = 1; + } else { + $i->{VBR} = 0; + } + + $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0; + $i->{PADDING} = $h->{padding_bit} ? 1 : 0; + $i->{STEREO} = $h->{mode} == 3 ? 0 : 1; + $i->{MODE} = $h->{mode}; + + $i->{SIZE} = $i->{VBR} == 1 && $vbr->{bytes} ? $vbr->{bytes} : $h->{size}; + $i->{OFFSET} = $h->{offset}; + + my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000); + $i->{FRAMES} = int($i->{VBR} == 1 && $vbr->{frames} + ? $vbr->{frames} + : $i->{SIZE} / ($h->{bitrate} / $mfs) + ); + + if ($i->{VBR} == 1) { + $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale}; + $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs; + if (not $h->{bitrate}) { + $@ = "Couldn't determine VBR bitrate"; + return undef; + } + } + + $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10; + $i->{SECS} = $h->{'length'} / 100; + $i->{MM} = int $i->{SECS} / 60; + $i->{SS} = int $i->{SECS} % 60; + $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000); +# $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS}); +# int($i->{MS} / 100 * 75); # is this right? + $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'}; + + $i->{BITRATE} = int $h->{bitrate}; + # should we just return if ! FRAMES? + $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES}; + $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}]; + + if ($lame) { + $i->{LAME} = $lame; + } + + return $i; +} + +sub _get_head { + my($byte) = @_; + my($bytes, $h); + + $bytes = _unpack_head($byte); + @$h{qw(IDR ID layer protection_bit + bitrate_index sampling_freq padding_bit private_bit + mode mode_extension copyright original + emphasis version_index bytes)} = ( + ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1, + ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1, + ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1, + $bytes&3, ($bytes>>19)&3, $bytes + ); + + $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}]; + $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}]; + + return $h; +} + +sub _is_mp3 { + my $h = $_[0] or return undef; + return ! ( # all below must be false + $h->{bitrate_index} == 0 + || + $h->{version_index} == 1 + || + ($h->{bytes} & 0xFFE00000) != 0xFFE00000 + || + !$h->{fs} + || + !$h->{bitrate} + || + $h->{bitrate_index} == 15 + || + !$h->{layer} + || + $h->{sampling_freq} == 3 + || + $h->{emphasis} == 2 + || + !$h->{bitrate_index} + || + ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000 + || + ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1) + # mode extension should only be applicable when mode = 1 + # however, failing just becuase mode extension is used when unneeded is a bit strict + # || + #($h->{mode_extension} != 0 && $h->{mode} != 1) + ); +} + +sub _vbr_seek { + my $fh = shift; + my $off = shift; + my $bytes = shift; + my $n = shift || 4; + + seek $fh, $$off, SEEK_SET; + read $fh, $$bytes, $n; + + $$off += $n; +} + +sub _get_vbr { + my ($fh, $h, $roff) = @_; + my ($off, $bytes, @bytes); + my %vbr = (is_vbr => 0); + + $off = $$roff; + + $off += 4; + + if ($h->{ID}) { # MPEG1 + $off += $h->{mode} == 3 ? 17 : 32; + } else { # MPEG2 + $off += $h->{mode} == 3 ? 9 : 17; + } + + _vbr_seek($fh, \$off, \$bytes); + + if ($bytes =~ /(?:Xing|Info)/) { + # Info is CBR + $vbr{is_vbr} = 1 if $bytes =~ /Xing/; + + _vbr_seek($fh, \$off, \$bytes); + $vbr{flags} = _unpack_head($bytes); + + if ($vbr{flags} & 1) { + _vbr_seek($fh, \$off, \$bytes); + $vbr{frames} = _unpack_head($bytes); + } + + if ($vbr{flags} & 2) { + _vbr_seek($fh, \$off, \$bytes); + $vbr{bytes} = _unpack_head($bytes); + } + + if ($vbr{flags} & 4) { + _vbr_seek($fh, \$off, \$bytes, 100); + # Not used right now ... + #$vbr{toc} = _unpack_head($bytes); + } + + if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst) + _vbr_seek($fh, \$off, \$bytes); + $vbr{scale} = _unpack_head($bytes); + } else { + $vbr{scale} = -1; + } + + $$roff = $off; + } elsif ($bytes =~ /(?:VBRI)/) { + $vbr{is_vbr} = 1; + + # Fraunhofer encoder uses VBRI format + # start with quality factor at position 8 + _vbr_seek($fh, \$off, \$bytes, 4); + _vbr_seek($fh, \$off, \$bytes, 2); + $vbr{scale} = unpack('l', pack('L', unpack('n', $bytes))); + + # Then Bytes, as position 10 + _vbr_seek($fh, \$off, \$bytes); + $vbr{bytes} = _unpack_head($bytes); + + # Finally Frames at position 14 + _vbr_seek($fh, \$off, \$bytes); + $vbr{frames} = _unpack_head($bytes); + + $$roff = $off; + } + + return \%vbr; +} + +# Read LAME info tag +# http://gabriel.mp3-tech.org/mp3infotag.html +sub _get_lame { + my($fh, $h, $roff) = @_; + + my($off, $bytes, @bytes, %lame); + + $off = $$roff; + + # Encode version, 9 bytes + _vbr_seek($fh, \$off, \$bytes, 9); + $lame{encoder_version} = $bytes; + + return unless $bytes =~ /^LAME/; + + # There's some stuff here but it's not too useful + _vbr_seek($fh, \$off, \$bytes, 12); + + # Encoder delays (used for gapless decoding) + _vbr_seek($fh, \$off, \$bytes, 3); + my $bin = unpack 'B*', $bytes; + $lame{start_delay} = unpack('N', pack('B32', substr('0' x 32 . substr($bin, 0, 12), -32))); + $lame{end_padding} = unpack('N', pack('B32', substr('0' x 32 . substr($bin, 12, 12), -32))); + + return \%lame; +} + +# _get_v2head(file handle, start offset in file); +# The start offset can be used to check ID3v2 headers anywhere +# in the MP3 (eg for 'update' frames). +sub _get_v2head { + my $fh = $_[0] or return; + + my $v2h = { + 'offset' => $_[1] || 0, + 'tag_size' => 0, + }; + + # check first three bytes for 'ID3' + seek($fh, $v2h->{offset}, SEEK_SET); + read($fh, my $header, 10); + + my $tag = substr($header, 0, 3); + + # (Note: Footers are dealt with in v2foot) + if ($v2h->{offset} == 0) { + + # JRF: Only check for special headers if we're at the start of the file. + if ($tag eq 'RIF' || $tag eq 'FOR') { + _find_id3_chunk($fh, $tag) or return; + $v2h->{offset} = tell $fh; + + read($fh, $header, 10); + $tag = substr($header, 0, 3); + } + } + + return if $tag ne 'ID3'; + + # get version + my ($major, $minor, $flags) = unpack ("x3CCC", $header); + + $v2h->{version} = sprintf("ID3v2.%d.%d", $major, $minor); + $v2h->{major_version} = $major; + $v2h->{minor_version} = $minor; + + # get flags + my @bits = split(//, unpack('b8', pack('v', $flags))); + + if ($v2h->{major_version} == 2) { + $v2h->{unsync} = $bits[7]; + $v2h->{compression} = $bits[6]; # Should be ignored - no defined form + $v2h->{ext_header} = 0; + $v2h->{experimental} = 0; + } else { + $v2h->{unsync} = $bits[7]; + $v2h->{ext_header} = $bits[6]; + $v2h->{experimental} = $bits[5]; + $v2h->{footer} = $bits[4] if $v2h->{major_version} == 4; + } + + # get ID3v2 tag length from bytes 7-10 + my $rawsize = substr($header, 6, 4); + + for my $b (unpack('C4', $rawsize)) { + + $v2h->{tag_size} = ($v2h->{tag_size} << 7) + $b; + } + + $v2h->{tag_size} += 10; # include ID3v2 header size + $v2h->{tag_size} += 10 if $v2h->{footer}; + + # JRF: I think this is done wrongly - this should be part of the main frame, + # and therefore under ID3v2.3 it's subject to unsynchronisation + # (ID3v2.3, section 3.2). + # FIXME. + + # get extended header size (2.3/2.4 only) + $v2h->{ext_header_size} = 0; + + if ($v2h->{ext_header}) { + my $filesize = -s $fh; + + read $fh, my $bytes, 4; + my @bytes = reverse unpack 'C4', $bytes; + + # use syncsafe bytes if using version 2.4 + my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256; + for my $i (0..3) { + $v2h->{ext_header_size} += $bytes[$i] * $bytesize ** $i; + } + + # Bug 4486 + # Don't try to read past the end of the file if we have a + # bogus extended header size. + if (($v2h->{ext_header_size} - 10 ) > -s $fh) { + + return $v2h; + } + + # Read the extended header + my $ext_data; + if ($v2h->{major_version} == 3) { + # On ID3v2.3 the extended header size excludes the whole header + read $fh, $bytes, 6 + $v2h->{ext_header_size}; + my @bits = split //, unpack 'b16', substr $bytes, 0, 2; + $v2h->{crc_present} = $bits[15]; + my $padding_size; + for my $i (0..3) { + + if (defined $bytes[2 + $i]) { + $padding_size += $bytes[2 + $i] * $bytesize ** $i; + } + } + $ext_data = substr $bytes, 6, $v2h->{ext_header_size} - $padding_size; + } + elsif ($v2h->{major_version} == 4) { + # On ID3v2.4, the extended header size includes the whole header + read $fh, $bytes, $v2h->{ext_header_size} - 4; + my @bits = split //, unpack 'b8', substr $bytes, 5, 1; + $v2h->{update} = $bits[6]; + $v2h->{crc_present} = $bits[5]; + $v2h->{tag_restrictions} = $bits[4]; + $ext_data = substr $bytes, 2, $v2h->{ext_header_size} - 6; + } + + # JRF: I'm not actually working out what the CRC or the tag + # restrictions are just yet. It doesn't seem to be + # all that worthwhile. + # However, if this is implemented... + # Under ID3v2.3, the CRC is not sync-safe (4 bytes). + # Under ID3v2.4, the CRC is sync-safe (5 bytes, excluding the flag data + # length) + # Under ID3v2.4, every flag byte that's set is given a flag data byte + # in the extended data area, the first byte of which is the size of + # the flag data (see ID3v2.4 section 3.2). + } + + return $v2h; +} + +# JRF: We assume that we have seeked to the expected EOF (ie start of the ID3v1 tag) +# The 'offset' value will hold the start of the ID3v1 header (NOT the footer) +# The 'tag_size' value will hold the entire tag size, including the footer. +sub _get_v2foot { + my $fh = $_[0] or return; + my($v2h, $bytes, @bytes); + my $eof; + + $eof = tell $fh; + + # check first three bytes for 'ID3' + seek $fh, $eof-10, SEEK_SET; # back 10 bytes for footer + read $fh, $bytes, 3; + + return undef unless $bytes eq '3DI'; + + # get version + read $fh, $bytes, 2; + $v2h->{version} = sprintf "ID3v2.%d.%d", + @$v2h{qw[major_version minor_version]} = + unpack 'c2', $bytes; + + # get flags + read $fh, $bytes, 1; + my @bits = split //, unpack 'b8', $bytes; + if ($v2h->{major_version} != 4) { + # JRF: This should never happen - only v4 tags should have footers. + # Think about raising some warnings or something ? + # print STDERR "Invalid ID3v2 footer version number\n"; + } else { + $v2h->{unsync} = $bits[7]; + $v2h->{ext_header} = $bits[6]; + $v2h->{experimental} = $bits[5]; + $v2h->{footer} = $bits[4]; + if (!$v2h->{footer}) + { + # JRF: This is an invalid footer marker; it doesn't make sense + # for the footer to not be marked as the tag having a footer + # so strictly it's an invalid tag. + # A warning might be nice, but for now we'll ignore. + # print STDERR "Warning: Footer doesn't have footer bit set\n"; + } + } + + # get ID3v2 tag length from bytes 7-10 + $v2h->{tag_size} = 10; # include ID3v2 header size + $v2h->{tag_size} += 10; # always account for the footer + read $fh, $bytes, 4; + @bytes = reverse unpack 'C4', $bytes; + foreach my $i (0 .. 3) { + # whoaaaaaa nellllllyyyyyy! + $v2h->{tag_size} += $bytes[$i] * 128 ** $i; + } + + # Note that there are no extended header details on the footer; it's + # just a copy of it so that clients can seek backward to find the + # footer's start. + + $v2h->{offset} = $eof - $v2h->{tag_size}; + + # Just to be really sure, read the start of the ID3v2.4 header here. + seek $fh, $v2h->{offset}, 0; # SEEK_SET + read $fh, $bytes, 3; + if ($bytes ne "ID3") { + # Not really an ID3v2.4 tag header; a warning would be nice but ignore + # for now. + # print STDERR "Invalid ID3v2 footer (header check) at " . $v2h->{offset} . "\n"; + return undef; + } + + # We could check more of the header. I'm not sure it's really worth it + # right now but at some point in the future checking the details match + # would be nice. + + return $v2h; + +}; + +sub _find_id3_chunk { + my($fh, $filetype) = @_; + my($bytes, $size, $tag, $pat, @mat); + + # CHANGE 10616 introduced a read optimization in _get_v2head: + # 10 bytes are read, not 3, so reading one here hoping to get the last letter of the + # tag is a bad idea, as it always fails... + +# read $fh, $bytes, 1; + if ($filetype eq 'RIF') { # WAV +# return 0 if $bytes ne 'F'; + $pat = 'a4V'; + @mat = ('id3 ', 'ID32'); + } elsif ($filetype eq 'FOR') { # AIFF +# return 0 if $bytes ne 'M'; + $pat = 'a4N'; + @mat = ('ID3 ', 'ID32'); + } + seek $fh, 12, SEEK_SET; # skip to the first chunk + + while ((read $fh, $bytes, 8) == 8) { + ($tag, $size) = unpack $pat, $bytes; + for my $mat ( @mat ) { + return 1 if $tag eq $mat; + } + seek $fh, $size, SEEK_CUR; + } + + return 0; +} + +sub _unpack_head { + unpack('l', pack('L', unpack('N', $_[0]))); +} + +sub _grab_int_16 { + my $data = shift; + my $value = unpack('s', pack('S', unpack('n',substr($$data,0,2)))); + $$data = substr($$data,2); + return $value; +} + +sub _grab_uint_16 { + my $data = shift; + my $value = unpack('S',substr($$data,0,2)); + $$data = substr($$data,2); + return $value; +} + +sub _grab_int_32 { + my $data = shift; + my $value = unpack('V',substr($$data,0,4)); + $$data = substr($$data,4); + return $value; +} + +# From getid3 - lyrics +# +# Just get the size and offset, so the APE tag can be parsed. +sub _parse_lyrics3_tag { + my ($fh, $filesize, $info) = @_; + + # end - ID3v1 - LYRICSEND - [Lyrics3size] + seek($fh, (0 - 128 - 9 - 6), SEEK_END); + read($fh, my $lyrics3_id3v1, 128 + 9 + 6); + + my $lyrics3_lsz = substr($lyrics3_id3v1, 0, 6); # Lyrics3size + my $lyrics3_end = substr($lyrics3_id3v1, 6, 9); # LYRICSEND or LYRICS200 + my $id3v1_tag = substr($lyrics3_id3v1, 15, 128); # ID3v1 + + my ($lyrics3_size, $lyrics3_offset, $lyrics3_version); + + # Lyrics3v1, ID3v1, no APE + if ($lyrics3_end eq 'LYRICSEND') { + + $lyrics3_size = 5100; + $lyrics3_offset = $filesize - 128 - $lyrics3_size; + $lyrics3_version = 1; + + } elsif ($lyrics3_end eq 'LYRICS200') { + + # Lyrics3v2, ID3v1, no APE + # LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' + $lyrics3_size = $lyrics3_lsz + 6 + length('LYRICS200'); + $lyrics3_offset = $filesize - 128 - $lyrics3_size; + $lyrics3_version = 2; + + } elsif (substr(reverse($lyrics3_id3v1), 0, 9) eq 'DNESCIRYL') { + + # Lyrics3v1, no ID3v1, no APE + $lyrics3_size = 5100; + $lyrics3_offset = $filesize - $lyrics3_size; + $lyrics3_version = 1; + $lyrics3_offset = $filesize - $lyrics3_size; + + } elsif (substr(reverse($lyrics3_id3v1), 0, 9) eq '002SCIRYL') { + + # Lyrics3v2, no ID3v1, no APE + # LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' > 15 = 6 + strlen('LYRICS200') + $lyrics3_size = reverse(substr(reverse($lyrics3_id3v1), 9, 6)) + 15; + $lyrics3_offset = $filesize - $lyrics3_size; + $lyrics3_version = 2; + } + + return $lyrics3_offset; +} + +sub _parse_ape_tag { + my ($fh, $filesize, $info) = @_; + + my $ape_tag_id = 'APETAGEX'; + my $id3v1_tag_size = 128; + my $ape_tag_header_size = 32; + my $lyrics3_tag_size = 10; + my $tag_offset_start = 0; + my $tag_offset_end = 0; + + if (my $offset = _parse_lyrics3_tag($fh, $filesize, $info)) { + + seek($fh, $offset - $ape_tag_header_size, SEEK_SET); + $tag_offset_end = $offset; + + } else { + + seek($fh, (0 - $id3v1_tag_size - $ape_tag_header_size - $lyrics3_tag_size), SEEK_END); + + read($fh, my $ape_footer_id3v1, $id3v1_tag_size + $ape_tag_header_size + $lyrics3_tag_size); + + if (substr($ape_footer_id3v1, (length($ape_footer_id3v1) - $id3v1_tag_size - $ape_tag_header_size), 8) eq $ape_tag_id) { + + $tag_offset_end = $filesize - $id3v1_tag_size; + + } elsif (substr($ape_footer_id3v1, (length($ape_footer_id3v1) - $ape_tag_header_size), 8) eq $ape_tag_id) { + + $tag_offset_end = $filesize; + } + + seek($fh, $tag_offset_end - $ape_tag_header_size, SEEK_SET); + } + + read($fh, my $ape_footer_data, $ape_tag_header_size); + + my $ape_footer = _parse_ape_header_or_footer($ape_footer_data); + + if (keys %{$ape_footer}) { + + my $ape_tag_data = ''; + + if ($ape_footer->{'flags'}->{'header'}) { + + seek($fh, ($tag_offset_end - $ape_footer->{'tag_size'} - $ape_tag_header_size), SEEK_SET); + + $tag_offset_start = tell($fh); + + read($fh, $ape_tag_data, $ape_footer->{'tag_size'} + $ape_tag_header_size); + + } else { + + $tag_offset_start = $tag_offset_end - $ape_footer->{'tag_size'}; + + seek($fh, $tag_offset_start, SEEK_SET); + + read($fh, $ape_tag_data, $ape_footer->{'tag_size'}); + } + + my $ape_header_data = substr($ape_tag_data, 0, $ape_tag_header_size, ''); + my $ape_header = _parse_ape_header_or_footer($ape_header_data); + + if ( defined $ape_header->{'version'} ) { + if ( $ape_header->{'version'} == 2000 ) { + $info->{'TAGVERSION'} = 'APEv2'; + } + else { + $info->{'TAGVERSION'} = 'APEv1'; + } + } + + if (defined $ape_header->{'tag_items'} && $ape_header->{'tag_items'} =~ /^\d+$/) { + + for (my $c = 0; $c < $ape_header->{'tag_items'}; $c++) { + + # Loop through the tag items + my $tag_len = _grab_int_32(\$ape_tag_data); + my $tag_flags = _grab_int_32(\$ape_tag_data); + + $ape_tag_data =~ s/^(.*?)\0//; + + my $tag_item_key = uc($1 || 'UNKNOWN'); + + $info->{$tag_item_key} = substr($ape_tag_data, 0, $tag_len, ''); + } + } + } + + seek($fh, 0, SEEK_SET); + + return 1; +} + +sub _parse_ape_header_or_footer { + my $bytes = shift; + my %data = (); + + if (substr($bytes, 0, 8, '') eq 'APETAGEX') { + + $data{'version'} = _grab_int_32(\$bytes); + $data{'tag_size'} = _grab_int_32(\$bytes); + $data{'tag_items'} = _grab_int_32(\$bytes); + $data{'global_flags'} = _grab_int_32(\$bytes); + + # trim the reseved bytes + _grab_int_32(\$bytes); + _grab_int_32(\$bytes); + + $data{'flags'}->{'header'} = ($data{'global_flags'} & 0x80000000) ? 1 : 0; + $data{'flags'}->{'footer'} = ($data{'global_flags'} & 0x40000000) ? 1 : 0; + $data{'flags'}->{'is_header'} = ($data{'global_flags'} & 0x20000000) ? 1 : 0; + } + + return \%data; +} + +sub _close { + my($file, $fh) = @_; + unless (ref $file) { # filehandle not passed + close $fh or carp "Problem closing '$file': $!"; + } +} + +BEGIN { + @mp3_genres = ( + 'Blues', + 'Classic Rock', + 'Country', + 'Dance', + 'Disco', + 'Funk', + 'Grunge', + 'Hip-Hop', + 'Jazz', + 'Metal', + 'New Age', + 'Oldies', + 'Other', + 'Pop', + 'R&B', + 'Rap', + 'Reggae', + 'Rock', + 'Techno', + 'Industrial', + 'Alternative', + 'Ska', + 'Death Metal', + 'Pranks', + 'Soundtrack', + 'Euro-Techno', + 'Ambient', + 'Trip-Hop', + 'Vocal', + 'Jazz+Funk', + 'Fusion', + 'Trance', + 'Classical', + 'Instrumental', + 'Acid', + 'House', + 'Game', + 'Sound Clip', + 'Gospel', + 'Noise', + 'AlternRock', + 'Bass', + 'Soul', + 'Punk', + 'Space', + 'Meditative', + 'Instrumental Pop', + 'Instrumental Rock', + 'Ethnic', + 'Gothic', + 'Darkwave', + 'Techno-Industrial', + 'Electronic', + 'Pop-Folk', + 'Eurodance', + 'Dream', + 'Southern Rock', + 'Comedy', + 'Cult', + 'Gangsta', + 'Top 40', + 'Christian Rap', + 'Pop/Funk', + 'Jungle', + 'Native American', + 'Cabaret', + 'New Wave', + 'Psychadelic', + 'Rave', + 'Showtunes', + 'Trailer', + 'Lo-Fi', + 'Tribal', + 'Acid Punk', + 'Acid Jazz', + 'Polka', + 'Retro', + 'Musical', + 'Rock & Roll', + 'Hard Rock', + ); + + @winamp_genres = ( + @mp3_genres, + 'Folk', + 'Folk-Rock', + 'National Folk', + 'Swing', + 'Fast Fusion', + 'Bebop', + 'Latin', + 'Revival', + 'Celtic', + 'Bluegrass', + 'Avantgarde', + 'Gothic Rock', + 'Progressive Rock', + 'Psychedelic Rock', + 'Symphonic Rock', + 'Slow Rock', + 'Big Band', + 'Chorus', + 'Easy Listening', + 'Acoustic', + 'Humour', + 'Speech', + 'Chanson', + 'Opera', + 'Chamber Music', + 'Sonata', + 'Symphony', + 'Booty Bass', + 'Primus', + 'Porn Groove', + 'Satire', + 'Slow Jam', + 'Club', + 'Tango', + 'Samba', + 'Folklore', + 'Ballad', + 'Power Ballad', + 'Rhythmic Soul', + 'Freestyle', + 'Duet', + 'Punk Rock', + 'Drum Solo', + 'Acapella', + 'Euro-House', + 'Dance Hall', + 'Goa', + 'Drum & Bass', + 'Club-House', + 'Hardcore', + 'Terror', + 'Indie', + 'BritPop', + 'Negerpunk', + 'Polsk Punk', + 'Beat', + 'Christian Gangsta Rap', + 'Heavy Metal', + 'Black Metal', + 'Crossover', + 'Contemporary Christian', + 'Christian Rock', + 'Merengue', + 'Salsa', + 'Thrash Metal', + 'Anime', + 'JPop', + 'Synthpop', + ); + + @t_bitrate = ([ + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] + ],[ + [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448], + [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], + [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] + ]); + + @t_sampling_freq = ( + [11025, 12000, 8000], + [undef, undef, undef], # reserved + [22050, 24000, 16000], + [44100, 48000, 32000] + ); + + @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 } + map { @$_ } @t_sampling_freq; + + @mp3_info_fields = qw( + VERSION + LAYER + STEREO + VBR + BITRATE + FREQUENCY + SIZE + OFFSET + SECS + MM + SS + MS + TIME + COPYRIGHT + PADDING + MODE + FRAMES + FRAME_LENGTH + VBR_SCALE + ); + + %rva2_channel_types = ( + 0x00 => 'OTHER', + 0x01 => 'MASTER', + 0x02 => 'FRONT_RIGHT', + 0x03 => 'FRONT_LEFT', + 0x04 => 'BACK_RIGHT', + 0x05 => 'BACK_LEFT', + 0x06 => 'FRONT_CENTER', + 0x07 => 'BACK_CENTER', + 0x08 => 'SUBWOOFER', + ); + + %v1_tag_fields = + (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4); + + @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE); + + %v2_to_v1_names = ( + # v2.2 tags + 'TT2' => 'TITLE', + 'TP1' => 'ARTIST', + 'TAL' => 'ALBUM', + 'TYE' => 'YEAR', + 'COM' => 'COMMENT', + 'TRK' => 'TRACKNUM', + 'TCO' => 'GENRE', # not clean mapping, but ... + # v2.3 tags + 'TIT2' => 'TITLE', + 'TPE1' => 'ARTIST', + 'TALB' => 'ALBUM', + 'TYER' => 'YEAR', + 'COMM' => 'COMMENT', + 'TRCK' => 'TRACKNUM', + 'TCON' => 'GENRE', + # v2.3 tags - needed for MusicBrainz + 'UFID' => 'Unique file identifier', + 'TXXX' => 'User defined text information frame', + ); + + %v2_tag_names = ( + # v2.2 tags + 'BUF' => 'Recommended buffer size', + 'CNT' => 'Play counter', + 'COM' => 'Comments', + 'CRA' => 'Audio encryption', + 'CRM' => 'Encrypted meta frame', + 'ETC' => 'Event timing codes', + 'EQU' => 'Equalization', + 'GEO' => 'General encapsulated object', + 'IPL' => 'Involved people list', + 'LNK' => 'Linked information', + 'MCI' => 'Music CD Identifier', + 'MLL' => 'MPEG location lookup table', + 'PIC' => 'Attached picture', + 'POP' => 'Popularimeter', + 'REV' => 'Reverb', + 'RVA' => 'Relative volume adjustment', + 'SLT' => 'Synchronized lyric/text', + 'STC' => 'Synced tempo codes', + 'TAL' => 'Album/Movie/Show title', + 'TBP' => 'BPM (Beats Per Minute)', + 'TCM' => 'Composer', + 'TCO' => 'Content type', + 'TCR' => 'Copyright message', + 'TDA' => 'Date', + 'TDY' => 'Playlist delay', + 'TEN' => 'Encoded by', + 'TFT' => 'File type', + 'TIM' => 'Time', + 'TKE' => 'Initial key', + 'TLA' => 'Language(s)', + 'TLE' => 'Length', + 'TMT' => 'Media type', + 'TOA' => 'Original artist(s)/performer(s)', + 'TOF' => 'Original filename', + 'TOL' => 'Original Lyricist(s)/text writer(s)', + 'TOR' => 'Original release year', + 'TOT' => 'Original album/Movie/Show title', + 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group', + 'TP2' => 'Band/Orchestra/Accompaniment', + 'TP3' => 'Conductor/Performer refinement', + 'TP4' => 'Interpreted, remixed, or otherwise modified by', + 'TPA' => 'Part of a set', + 'TPB' => 'Publisher', + 'TRC' => 'ISRC (International Standard Recording Code)', + 'TRD' => 'Recording dates', + 'TRK' => 'Track number/Position in set', + 'TSI' => 'Size', + 'TSS' => 'Software/hardware and settings used for encoding', + 'TT1' => 'Content group description', + 'TT2' => 'Title/Songname/Content description', + 'TT3' => 'Subtitle/Description refinement', + 'TXT' => 'Lyricist/text writer', + 'TXX' => 'User defined text information frame', + 'TYE' => 'Year', + 'UFI' => 'Unique file identifier', + 'ULT' => 'Unsychronized lyric/text transcription', + 'WAF' => 'Official audio file webpage', + 'WAR' => 'Official artist/performer webpage', + 'WAS' => 'Official audio source webpage', + 'WCM' => 'Commercial information', + 'WCP' => 'Copyright/Legal information', + 'WPB' => 'Publishers official webpage', + 'WXX' => 'User defined URL link frame', + + # v2.3 tags + 'AENC' => 'Audio encryption', + 'APIC' => 'Attached picture', + 'COMM' => 'Comments', + 'COMR' => 'Commercial frame', + 'ENCR' => 'Encryption method registration', + 'EQUA' => 'Equalization', + 'ETCO' => 'Event timing codes', + 'GEOB' => 'General encapsulated object', + 'GRID' => 'Group identification registration', + 'IPLS' => 'Involved people list', + 'LINK' => 'Linked information', + 'MCDI' => 'Music CD identifier', + 'MLLT' => 'MPEG location lookup table', + 'OWNE' => 'Ownership frame', + 'PCNT' => 'Play counter', + 'POPM' => 'Popularimeter', + 'POSS' => 'Position synchronisation frame', + 'PRIV' => 'Private frame', + 'RBUF' => 'Recommended buffer size', + 'RVAD' => 'Relative volume adjustment', + 'RVRB' => 'Reverb', + 'SYLT' => 'Synchronized lyric/text', + 'SYTC' => 'Synchronized tempo codes', + 'TALB' => 'Album/Movie/Show title', + 'TBPM' => 'BPM (beats per minute)', + 'TCOM' => 'Composer', + 'TCON' => 'Content type', + 'TCOP' => 'Copyright message', + 'TDAT' => 'Date', + 'TDLY' => 'Playlist delay', + 'TENC' => 'Encoded by', + 'TEXT' => 'Lyricist/Text writer', + 'TFLT' => 'File type', + 'TIME' => 'Time', + 'TIT1' => 'Content group description', + 'TIT2' => 'Title/songname/content description', + 'TIT3' => 'Subtitle/Description refinement', + 'TKEY' => 'Initial key', + 'TLAN' => 'Language(s)', + 'TLEN' => 'Length', + 'TMED' => 'Media type', + 'TOAL' => 'Original album/movie/show title', + 'TOFN' => 'Original filename', + 'TOLY' => 'Original lyricist(s)/text writer(s)', + 'TOPE' => 'Original artist(s)/performer(s)', + 'TORY' => 'Original release year', + 'TOWN' => 'File owner/licensee', + 'TPE1' => 'Lead performer(s)/Soloist(s)', + 'TPE2' => 'Band/orchestra/accompaniment', + 'TPE3' => 'Conductor/performer refinement', + 'TPE4' => 'Interpreted, remixed, or otherwise modified by', + 'TPOS' => 'Part of a set', + 'TPUB' => 'Publisher', + 'TRCK' => 'Track number/Position in set', + 'TRDA' => 'Recording dates', + 'TRSN' => 'Internet radio station name', + 'TRSO' => 'Internet radio station owner', + 'TSIZ' => 'Size', + 'TSRC' => 'ISRC (international standard recording code)', + 'TSSE' => 'Software/Hardware and settings used for encoding', + 'TXXX' => 'User defined text information frame', + 'TYER' => 'Year', + 'UFID' => 'Unique file identifier', + 'USER' => 'Terms of use', + 'USLT' => 'Unsychronized lyric/text transcription', + 'WCOM' => 'Commercial information', + 'WCOP' => 'Copyright/Legal information', + 'WOAF' => 'Official audio file webpage', + 'WOAR' => 'Official artist/performer webpage', + 'WOAS' => 'Official audio source webpage', + 'WORS' => 'Official internet radio station homepage', + 'WPAY' => 'Payment', + 'WPUB' => 'Publishers official webpage', + 'WXXX' => 'User defined URL link frame', + + # v2.4 additional tags + # note that we don't restrict tags from 2.3 or 2.4, + 'ASPI' => 'Audio seek point index', + 'EQU2' => 'Equalisation (2)', + 'RVA2' => 'Relative volume adjustment (2)', + 'SEEK' => 'Seek frame', + 'SIGN' => 'Signature frame', + 'TDEN' => 'Encoding time', + 'TDOR' => 'Original release time', + 'TDRC' => 'Recording time', + 'TDRL' => 'Release time', + 'TDTG' => 'Tagging time', + 'TIPL' => 'Involved people list', + 'TMCL' => 'Musician credits list', + 'TMOO' => 'Mood', + 'TPRO' => 'Produced notice', + 'TSOA' => 'Album sort order', + 'TSOP' => 'Performer sort order', + 'TSOT' => 'Title sort order', + 'TSST' => 'Set subtitle', + + # grrrrrrr + 'COM ' => 'Broken iTunes comments', + ); +} + +1; + +__END__ + +=pod + +=back + +=head1 TROUBLESHOOTING + +If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">). +If you cannot figure out why it does not work for you, please put the MP3 file in +a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me +mail regarding where I can get the file, with a detailed description of the problem. + +If I download the file, after debugging the problem I will not keep the MP3 file +if it is not legal for me to have it. Just let me know if it is legal for me to +keep it or not. + + +=head1 TODO + +=over 4 + +=item ID3v2 Support + +Still need to do more for reading tags, such as using Compress::Zlib to decompress +compressed tags. But until I see this in use more, I won't bother. If something +does not work properly with reading, follow the instructions above for +troubleshooting. + +ID3v2 I<writing> is coming soon. + +=item Get data from scalar + +Instead of passing a file spec or filehandle, pass the +data itself. Would take some work, converting the seeks, etc. + +=item Padding bit ? + +Do something with padding bit. + +=item Test suite + +Test suite could use a bit of an overhaul and update. Patches very welcome. + +=over 4 + +=item * + +Revamp getset.t. Test all the various get_mp3tag args. + +=item * + +Test Unicode. + +=item * + +Test OOP API. + +=item * + +Test error handling, check more for missing files, bad MP3s, etc. + +=back + +=item Other VBR + +Right now, only Xing VBR is supported. + +=back + + +=head1 THANKS + +Edward Allen, +Vittorio Bertola, +Michael Blakeley, +Per Bolmstedt, +Tony Bowden, +Tom Brown, +Sergio Camarena, +Chris Dawson, +Kevin Deane-Freeman, +Anthony DiSante, +Luke Drumm, +Kyle Farrell, +Jeffrey Friedl, +brian d foy, +Ben Gertzfield, +Brian Goodwin, +Andy Grundman, +Todd Hanneken, +Todd Harris, +Woodrow Hill, +Kee Hinckley, +Roman Hodek, +Ilya Konstantinov, +Peter Kovacs, +Johann Lindvall, +Alex Marandon, +Peter Marschall, +michael, +Trond Michelsen, +Dave O'Neill, +Christoph Oberauer, +Jake Palmer, +Andrew Phillips, +David Reuteler, +John Ruttenberg, +Matthew Sachs, +scfc_de, +Hermann Schwaerzler, +Chris Sidi, +Roland Steinbach, +Brian S. Stephan, +Stuart, +Dan Sully, +Jeffery Sumler, +Predrag Supurovic, +Bogdan Surdu, +Pierre-Yves Thoulon, +tim, +Pass F. B. Travis, +Tobias Wagener, +Ronan Waide, +Andy Waite, +Ken Williams, +Ben Winslow, +Meng Weng Wong, +Justin Fletcher. + +=head1 CURRENT AUTHOR + +Dan Sully E<lt>daniel | at | cpan.orgE<gt> & Logitech. + +=head1 AUTHOR EMERITUS + +Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/ + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 2006-2008 Dan Sully & Logitech. All rights reserved. + +Copyright (c) 1998-2005 Chris Nandor. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the same terms as Perl itself. + +=head1 SEE ALSO + +=over 4 + +=item Logitech/Slim Devices + + http://www.slimdevices.com/ + +=item mp3tools + + http://www.zevils.com/linux/mp3tools/ + +=item mpgtools + + http://www.dv.co.yu/mpgscript/mpgtools.htm + http://www.dv.co.yu/mpgscript/mpeghdr.htm + +=item mp3tool + + http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html + +=item ID3v2 + + http://www.id3.org/ + +=item Xing Variable Bitrate + + http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/ + +=item MP3Ext + + http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/ + +=item Xmms + + http://www.xmms.org/ + + +=back + +=cut diff --git a/fhem/FHEM/lib/MP3/Tag.pm b/fhem/FHEM/lib/MP3/Tag.pm new file mode 100644 index 000000000..87b43653d --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag.pm @@ -0,0 +1,3558 @@ + +package MP3::Tag; + +# Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Artistic License, distributed +# with Perl. + +################ +# +# provides a general interface for different modules, which can read tags +# +# at the moment MP3::Tag works with MP3::Tag::ID3v1 and MP3::Tag::ID3v2 + +use strict; + +{ + package MP3::Tag::__hasparent; + sub parent_ok { + my $self = shift; + $self->{parent} and $self->{parent}->proxy_ok; + } + sub get_config { + my $self = shift; + return $MP3::Tag::config{shift()} unless $self->parent_ok; + return $self->{parent}->get_config(@_); + } + *get_config1 = \&MP3::Tag::Implemenation::get_config1; +} + +use MP3::Tag::ID3v1; +use MP3::Tag::ID3v2; +use MP3::Tag::File; +use MP3::Tag::Inf; +use MP3::Tag::CDDB_File; +use MP3::Tag::Cue; +use MP3::Tag::ParseData; +use MP3::Tag::ImageSize; +use MP3::Tag::ImageExifTool; +use MP3::Tag::LastResort; + +use vars qw/$VERSION @ISA/; +$VERSION="1.13"; +@ISA = qw( MP3::Tag::User MP3::Tag::Site MP3::Tag::Vendor + MP3::Tag::Implemenation ); # Make overridable +*config = \%MP3::Tag::Implemenation::config; + +package MP3::Tag::Implemenation; # XXXX Old mispring... +use vars qw/%config/; +%config = ( autoinfo => [qw( ParseData ID3v2 ID3v1 ImageExifTool + CDDB_File Inf Cue ImageSize + filename LastResort )], + cddb_files => [qw(audio.cddb cddb.out cddb.in)], + v2title => [qw(TIT1 TIT2 TIT3)], + composer => ['TCOM|a'], + performer => ['TXXX[TPE1]|TPE1|a'], + extension => ['\.(?!\d+\b)\w{1,4}$'], + parse_data => [], + parse_split => ["\n"], + encoded_v1_fits => [0], + parse_filename_ignore_case => [1], + parse_filename_merge_dots => [1], + parse_join => ['; '], + year_is_timestamp => [1], + comment_remove_date => [0], + id3v2_frame_empty_ok => [0], + id3v2_minpadding => [128], + id3v2_sizemult => [512], + id3v2_shrink => [0], + id3v2_mergepadding => [0], + id3v23_unsync_size_w => [0], + id3v23_unsync => [1], + parse_minmatch => [0], + update_length => [1], + default_language => ['XXX'], + default_descr_c => [''], + person_frames => [qw{ TEXT TCOM TXXX[TPE1] TPE1 + TPE3 TOPE TOLY TMCL TIPL TENC + TXXX[person-file-by] }], + id3v2_frames_autofill => [qw{ TXXX[MCDI-fulltoc] 1 TXXX[cddb_id] 0 + TXXX[cdindex_id] 0 }], + id3v2_set_trusted_encoding0 => [1], + id3v2_fix_encoding_on_edit => [1], + name_for_field_normalization => ['%{composer}'], + local_cfg_file => ['~/.mp3tagprc'], + extra_config_keys => [], + is_writable => ['writable_by_extension'], + # ExifTool says: ID3 may be in MP3/MPEG/AIFF/OGG/FLAC/APE/RealAudio (MPC). + writable_extensions => [qw(mp3 mp2 id3 tag ogg mpg mpeg + mp4 aiff flac ape ram mpc)], + ); +{ + my %e; + for my $t (qw(V1 V2 FILENAME FILES INF CDDB_FILE CUE)) { + $e{$t} = $ENV{"MP3TAG_DECODE_${t}_DEFAULT"}; + $e{$t} = $ENV{MP3TAG_DECODE_DEFAULT} unless defined $e{$t}; + $config{"decode_encoding_" . lc $t} = [$e{$t}] if $e{$t}; + } + $e{eV1} = $ENV{MP3TAG_ENCODE_V1_DEFAULT}; + $e{eV1} = $ENV{MP3TAG_ENCODE_DEFAULT} unless defined $e{eV1}; + $e{eV1} = $e{V1} unless defined $e{eV1}; + $config{encode_encoding_v1} = [$e{eV1}] if $e{eV1}; + + $e{eF} = $ENV{MP3TAG_ENCODE_FILES_DEFAULT}; + $e{eF} = $ENV{MP3TAG_ENCODE_DEFAULT} unless defined $e{eF}; + $e{eF} = $e{FILES} unless defined $e{eF}; + $config{encode_encoding_files} = [$e{eF}] if $e{eF}; +} + +=pod + +=head1 NAME + +MP3::Tag - Module for reading tags of MP3 audio files + +=head1 SYNOPSIS + + use MP3::Tag; + + $mp3 = MP3::Tag->new($filename); + + # get some information about the file in the easiest way + ($title, $track, $artist, $album, $comment, $year, $genre) = $mp3->autoinfo(); + # Or: + $comment = $mp3->comment(); + $dedicated_to + = $mp3->select_id3v2_frame_by_descr('COMM(fre,fra,eng,#0)[dedicated to]'); + + $mp3->title_set('New title'); # Edit in-memory copy + $mp3->select_id3v2_frame_by_descr('TALB', 'New album name'); # Edit in memory + $mp3->select_id3v2_frame_by_descr('RBUF', $n1, $n2, $n3); # Edit in memory + $mp3->update_tags({year => 1866}); # Edit in-memory, and commit to file + $mp3->update_tags(); # Commit to file + +The following low-level access code is discouraged; better use title() +etc., title_set() etc., update_tags(), select_id3v2_frame_by_descr() +etc. methods on the wrapper $mp3: + + # scan file for existing tags + $mp3->get_tags; + + if (exists $mp3->{ID3v1}) { + # read some information from the tag + $id3v1 = $mp3->{ID3v1}; # $id3v1 is only a shortcut for $mp3->{ID3v1} + print $id3v1->title; + + # change the tag contents + $id3v1->all("Song","Artist","Album",2001,"Comment",10,"Top 40"); + $id3v1->write_tag; + } + + if (exists $mp3->{ID3v2}) { + # read some information from the tag + ($name, $info) = $mp3->{ID3v2}->get_frame("TIT2"); + # delete the tag completely from the file + $mp3->{ID3v2}->remove_tag; + } else { + # create a new tag + $mp3->new_tag("ID3v2"); + $mp3->{ID3v2}->add_frame("TALB", "Album title"); + $mp3->{ID3v2}->write_tag; + } + + $mp3->close(); + +Please consider using the script F<mp3info2>; it allows simple access +to most features of this module via command-line options; see +L<mp3info2>. + +=head1 AUTHORS + +Thomas Geffert, thg@users.sourceforge.net +Ilya Zakharevich, ilyaz@cpan.org + +=head1 DESCRIPTION + +C<MP3::Tag> is a wrapper module to read different tags of mp3 files. +It provides an easy way to access the functions of separate modules which +do the handling of reading/writing the tags itself. + +At the moment MP3::Tag::ID3v1 and MP3::Tag::ID3v2 are supported for +read and write; MP3::Tag::ImageExifTool, MP3::Tag::Inf, MP3::Tag::CDDB_File, +MP3::Tag::File, MP3::Tag::Cue, MP3::Tag::ImageSize, MP3::Tag::LastResort +are supported for read access (the information obtained by +L<Image::ExifTool|Image::ExifTool> (if present), parsing CDDB files, +F<.inf> file, the filename, and F<.cue> file, and obtained via +L<Image::Size|Image::Size>) (if present). + +=over 4 + +=item new() + + $mp3 = MP3::Tag->new($filename); + +Creates a mp3-object, which can be used to retrieve/set +different tags. + +=cut + +sub rel2abs ($) { + shift; + if (eval {require File::Spec; File::Spec->can('rel2abs')}) { + File::Spec->rel2abs(shift); + } else { +# require Cwd; +# Cwd::abs_path(shift); + shift; + } +} + +sub new { + my $class = shift; + my $filename = shift; + my $mp3data; + my $self = {}; + bless $self, $class; + my $proxy = MP3::Tag::__proxy->new($self); + if (-f $filename or -c $filename) { + $mp3data = MP3::Tag::File->new_with_parent($filename, $proxy); + } + # later it should hopefully possible to support also http/ftp sources + # with a MP3::Tag::Net module or something like that + if ($mp3data) { + %$self = (filename => $mp3data, + ofilename => $filename, + abs_filename => $class->rel2abs($filename), + __proxy => $proxy); + return $self; + } + return undef; +} + +{ # Proxy class: to have only one place where to weaken/localize the reference + # $obj->[0] must be settable to the handle (not needed if weakening succeeds) + package MP3::Tag::__proxy; + use vars qw/$AUTOLOAD/; + + my $skip_weaken = $ENV{MP3TAG_SKIP_WEAKEN}; + sub new { + my ($class, $handle) = (shift,shift); + my $self = bless [$handle], $class; + #warn("weaken() failed, falling back"), + return bless [], $class if $skip_weaken or not + eval {require Scalar::Util; Scalar::Util::weaken($self->[0]); 1}; + $self; + } + sub DESTROY {} + sub proxy_ok { shift->[0] } + sub AUTOLOAD { + my $self = shift; + die "local_proxy not initialized" unless $self->[0]; + (my $meth = $AUTOLOAD) =~ s/.*:://; + my $smeth = $self->[0]->can($meth); + die "proxy can't find the method $meth" unless $smeth; + unshift @_, $self->[0]; + goto &$smeth; + } +} + +sub proxy_ok { 1 } # We can always be a proxy to ourselves... ;-) + +=pod + +=item get_tags() + + [old name: getTags() . The old name is still available, but its use is not advised] + + @tags = $mp3->get_tags; + +Checks which tags can be found in the mp3-object. It returns +a list @tags which contains strings identifying the found tags, like +"ID3v1", "ID3v2", "Inf", or "CDDB_File" (the last but one if the F<.inf> +information file with the same basename as MP3 file is found). + +Each found tag can then be accessed with $mp3->{tagname} , where tagname is +a string returned by get_tags ; + +Use the information found in L<MP3::Tag::ID3v1>, L<MP3::Tag::ID3v2> and +L<MP3::Tag::Inf>, L<MP3::Tag::CDDB_File>, L<MP3::Tag::Cue> to see what you can do with the tags. + +=cut + +################ tag subs + +sub get_tags { + my $self = shift; + return @{$self->{gottags}} if exists $self->{gottags}; + my (@IDs, $id); + + # Will not create a reference loop + local $self->{__proxy}[0] = $self unless $self->{__proxy}[0] or $ENV{MP3TAG_TEST_WEAKEN}; + for $id (qw(ParseData ID3v2 ID3v1 ImageExifTool Inf CDDB_File Cue ImageSize LastResort)) { + my $ref = "MP3::Tag::$id"->new_with_parent($self->{filename}, $self->{__proxy}); + next unless defined $ref; + $self->{$id} = $ref; + push @IDs, $id; + } + $self->{gottags} = [@IDs]; + return @IDs; +} + +sub _get_tag { + my $self = shift; + $self->{shift()}; +} + +# keep old name for a while +*getTags = \&get_tags; + +=item new_fake + + $obj = MP3::Tag->new_fake(); + +This method produces a "fake" MP3::Tag object which behaves as an MP3 +file without tags. Give a TRUE optional argument if you want to set +some properties of this object. + +=cut + +sub new_fake { + my ($class, $settable) = (shift, shift); + my %h = (gottags => []); + my $self = bless \%h, $class; + if ($settable) { + $h{__proxy} = MP3::Tag::__proxy->new($self); + $h{ParseData} = MP3::Tag::ParseData->new_with_parent(undef, $h{__proxy}); + } + \%h; +} + + +=pod + +=item new_tag() + + [old name: newTag() . The old name is still available, but its use is not advised] + + $tag = $mp3->new_tag($tagname); + +Creates a new tag of the given type $tagname. You +can access it then with $mp3->{$tagname}. At the +moment ID3v1 and ID3v2 are supported as tagname. + +Returns an tag-object: $mp3->{$tagname}. + +=cut + +sub new_tag { + my $self = shift; + my $whichTag = shift; + if ($whichTag =~ /1/) { + $self->{ID3v1}= MP3::Tag::ID3v1->new($self->{filename},1); + return $self->{ID3v1}; + } elsif ($whichTag =~ /2/) { + $self->{ID3v2}= MP3::Tag::ID3v2->new($self->{filename},1); + return $self->{ID3v2}; + } +} + +# keep old name for a while +*newTag = \&new_tag; + +#only as a shortcut to {filename}->close to explicitly close a file + +=pod + +=item close() + + $mp3->close; + +You can use close() to explicitly close a file. Normally this is done +automatically by the module, so that you do not need to do this. + +=cut + +sub close { + my $self=shift; + $self->{filename}->close; +} + +=pod + +=item genres() + + $allgenres = $mp3->genres; + $genreName = $mp3->genres($genreID); + $genreID = $mp3->genres($genreName); + +Returns a list of all genres (reference to an array), or the according +name or id to a given id or name. + +This function is only a shortcut to MP3::Tag::ID3v1->genres. + +This can be also called as MP3::Tag->genres; + +=cut + +sub genres { + # returns all genres, or if a parameter is given, the according genre + my $self=shift; + return MP3::Tag::ID3v1::genres(shift); +} + +=pod + +=item autoinfo() + + ($title, $track, $artist, $album, $comment, $year, $genre) = $mp3->autoinfo(); + $info_hashref = $mp3->autoinfo(); + +autoinfo() returns information about the title, track number, +artist, album name, the file comment, the year and genre. It can get this +information from an ID3v1-tag, an ID3v2-tag, from CDDB file, from F<.inf>-file, +and from the filename itself. + +It will as default first try to find a ID3v2-tag to get this +information. If this cannot be found it tries to find a ID3v1-tag, then +to read an CDDB file, an F<.inf>-file, and +if these are not present either, it will use the filename to retrieve +the title, track number, artist, album name. The comment, year and genre +are found differently, via the C<comment>, C<year> and C<genre> methods. + +You can change the order of lookup with the config() command. + +autoinfo() returns an array with the information or a hashref. The hash +has four keys 'title', 'track', 'artist' and 'album' where the information is +stored. If comment, year or genre are found, the hash will have keys +'comment' and/or 'year' and/or 'genre' too. + +If an optional argument C<'from'> is given, the returned values (title, +track number, artist, album name, the file comment, the year and genre) are +array references with the first element being the value, the second the +tag (C<ID3v2> or C<ID3v1> or C<Inf> or C<CDDB_File> or C<Cue> or C<filename>) from which +it is taken. + +(Deprecated name 'song' can be used instead of 'title' as well.) + +=cut + +sub autoinfo() { + my ($self, $from) = (shift, shift); + my (@out, %out); + + for my $elt ( qw( title track artist album comment year genre ) ) { + my $out = $self->$elt($from); + if (wantarray) { + push @out, $out; + } elsif (defined $out and length $out) { + $out{$elt} = $out; + } + } + $out{song} = $out{title} if exists $out{title}; + + return wantarray ? @out : \%out; +} + +=item comment() + + $comment = $mp3->comment(); # empty string unless found + +comment() returns comment information. It can get this information from an +ID3v1-tag, or an ID3v2-tag (from C<COMM> frame with empty <short> field), +CDDB file (from C<EXTD> or C<EXTT> fields), or F<.inf>-file (from +C<Trackcomment> field). + +It will as default first try to find a ID3v2-tag to get this +information. If no comment is found there, it tries to find it in a ID3v1-tag, +if none present, will try CDDB file, then F<.inf>-file. It returns an empty string if +no comment is found. + +You can change the order of this with the config() command. + +If an optional argument C<'from'> is given, returns an array reference with +the first element being the value, the second the tag (ID3v2 or ID3v1) from +which the value is taken. + +=cut + +=item year() + + $year = $mp3->year(); # empty string unless found + +year() returns the year information. It can get this information from an +ID3v2-tag, or ID3v1-tag, or F<.inf>-file, or filename. + +It will as default first try to find a ID3v2-tag to get this +information. If no year is found there, it tries to find it in a ID3v1-tag, +if none present, will try CDDB file, then F<.inf>-file, +then by parsing the file name. It returns an empty string if no year is found. + +You can change the order of this with the config() command. + +If an optional argument C<'from'> is given, returns an array reference with +the first element being the value, the second the tag (ID3v2 or ID3v1 or +filename) from which the value is taken. + +=item comment_collection(), comment_track(), title_track(). artist_collection() + +access the corresponding fields returned by parse() method of CDDB_File. + +=item cddb_id(), cdindex_id() + +access the corresponding methods of C<ID3v2>, C<Inf> or C<CDDB_File>. + +=item title_set(), artist_set(), album_set(), year_set(), comment_set(), track_set(), genre_set() + + $mp3->title_set($newtitle, [$force_id3v2]); + +Set the corresponding value in ID3v1 tag, and, if the value does not fit, +or force_id3v2 is TRUE, in the ID3v2 tag. Changes are made to in-memory +copy only. To propagate to the file, use update_tags() or similar methods. + +=item track1() + +Same as track(), but strips trailing info: if track() returns C<3/12> +(which means track 3 of 12), this method returns C<3>. + +=item track2() + +Returns the second part of track number (compare with track1()). + +=item track0() + +Same as track1(), but pads with leading 0s to width of track2(); takes an +optional argument (default is 2) giving the pad width in absense of track2(). + +=item disk1(), disk2() + +Same as track1(), track2(), but with disk-number instead of track-number +(stored in C<TPOS> ID3v2 frame). + +=item disk_alphanum() + +Same as disk1(), but encodes a non-empty result as a letter (1 maps to C<a>, +2 to C<b>, etc). If number of disks is more than 26, falls back to numeric +(e.g, C<3/888> will be encoded as C<003>). + +=cut + +sub track1 ($) { + my $r = track(@_); + $r =~ s(/.*)()s; + $r; +} + +sub track2 ($) { + my $r = track(@_); + return '' unless $r =~ s(^.*?/)()s; + $r; +} + +sub track0 ($) { + my $self = shift; + my $d = (@_ ? shift() : 2); + my $r = $self->track(); + return '' unless defined $r; + (my $r1 = $r) =~ s(/.*)()s; + $r = 'a' x $d unless $r =~ s(^.*?/)()s; + my $l = length $r; + sprintf "%0${l}d", $r1; +} + +sub disk1 ($) { + my $self = shift; + my $r = $self->select_id3v2_frame('TPOS'); + return '' unless defined $r; + $r =~ s(/.*)()s; + $r; +} + +sub disk2 ($) { + my $self = shift; + my $r = $self->select_id3v2_frame('TPOS'); + return '' unless defined $r; + return '' unless $r =~ s(^.*?/)()s; + $r; +} + +sub disk_alphanum ($) { + my $self = shift; + my $r = $self->select_id3v2_frame('TPOS'); + return '' unless defined $r; + (my $r1 = $r) =~ s(/.*)()s; + $r = $r1 unless $r =~ s(^.*?/)()s; # max(disk2, disk1) + return chr(ord('a') - 1 + $r1) if $r <= 26; + my $l = length $r; + sprintf "%0${l}d", $r1; +} + +my %ignore_0length = qw(ID3v1 1 CDDB_File 1 Inf 1 Cue 1 ImageSize 1 ImageExifTool 1); + +sub auto_field($;$) { + my ($self, $elt, $from) = (shift, shift, shift); + local $self->{__proxy}[0] = $self unless $self->{__proxy}[0] or $ENV{MP3TAG_TEST_WEAKEN}; + + my $parts = $self->get_config($elt) || $self->get_config('autoinfo'); + $self->get_tags; + + my $do_can = ($elt =~ /^(cd\w+_id|height|width|bit_depth|mime_type|img_type|_duration)$/); + foreach my $part (@$parts) { + next unless exists $self->{$part}; + next if $do_can and not $self->{$part}->can($elt); + next unless defined (my $out = $self->{$part}->$elt()); + # Ignore 0-length answers from ID3v1, ImageExifTool, CDDB_File, Cue, ImageSize, and Inf + next if not length $out and $ignore_0length{$part}; # These return '' + return [$out, $part] if $from; + return $out; + } + return ''; +} + +for my $elt ( qw( title track artist album comment year genre ) ) { + no strict 'refs'; + *$elt = sub (;$) { + my $self = shift; + my $translate = ($self->get_config("translate_$elt") || [])->[0] || sub {$_[1]}; + return &$translate($self, $self->auto_field($elt, @_)); + } +} + +my %hide_meth = qw(mime_type _mime_type); + +for my $elt ( qw( cddb_id cdindex_id height width bit_depth mime_type img_type _duration ) ) { + no strict 'refs'; + *{$hide_meth{$elt} || $elt} = sub (;$) { + my $self = shift; + return $self->auto_field($elt, @_); + } +} + +for my $elt ( qw( comment_collection comment_track title_track artist_collection ) ) { + no strict 'refs'; + my ($tr) = ($elt =~ /^(\w+)_/); + *$elt = sub (;$) { + my $self = shift; + local $self->{__proxy}[0] = $self unless $self->{__proxy}[0] or $ENV{MP3TAG_TEST_WEAKEN}; + $self->get_tags; + return unless exists $self->{CDDB_File}; + my $v = $self->{CDDB_File}->parse($elt); + return unless defined $v; + my $translate = ($self->get_config("translate_$tr") || [])->[0] || sub {$_[1]}; + return &$translate( $self, $v ); + } +} + +for my $elt ( qw(title artist album year comment track genre) ) { + no strict 'refs'; + *{"${elt}_set"} = sub ($$;$) { + my ($mp3, $val, $force2) = (shift, shift, shift); + + $mp3->get_tags; + $mp3->new_tag("ID3v1") unless exists $mp3->{ID3v1}; + $mp3->{ID3v1}->$elt( $val ); + + return 1 + if not $force2 and $mp3->{ID3v1}->fits_tag({$elt => $val}) + and not exists $mp3->{ID3v2}; + + $mp3->new_tag("ID3v2") unless exists $mp3->{ID3v2}; + $mp3->{ID3v2}->$elt( $val ); + } +} + +sub aspect_ratio ($) { + my $self = shift; + my ($w, $h) = ($self->width, $self->height); + return unless $w and $h; + $w/$h; +} + +sub aspect_ratio_inverted ($) { + my $r = shift->aspect_ratio or return; + 1/$r; +} + +sub aspect_ratio3 ($) { + my $r = shift->aspect_ratio(); + $r ? sprintf '%.3f', $r : $r; +} + +=item mime_type( [$lazy] ) + +Returns the MIME type as a string. Returns C<application/octet-stream> +for unrecognized types. If not $lazy, will try harder (via ExifTool, if +needed). + +=item mime_Pretype( [$lazy] ) + +Returns uppercased first component of MIME type. + +=cut + +sub mime_Pretype ($;$) { + my $r = shift->mime_type(shift); + $r =~ s,/.*,,s; + ucfirst lc $r +} + +sub mime_type ($;$) { # _mime_type goes thru auto_field 'mime_type' + my ($self, $lazy) = (shift, shift); + $self->get_tags; + my $h = $self->{header}; + my $t = $h && $self->_Data_to_MIME($h, 1); + return $t if $t; + return((!$lazy && $self->_mime_type()) || 'application/octet-stream'); +} + +=item genre() + + $genre = $mp3->genre(); # empty string unless found + +genre() returns the genre string. It can get this information from an +ID3v2-tag or ID3v1-tag. + +It will as default first try to find a ID3v2-tag to get this +information. If no genre is found there, it tries to find it in a ID3v1-tag, +if none present, will try F<.inf>-file, +It returns an empty string if no genre is found. + +You can change the order of this with the config() command. + +If an optional argument C<'from'> is given, returns an array reference with +the first element being the value, the second the tag (ID3v2 or ID3v1 or +filename) from which the value is taken. + +=item composer() + + $composer = $mp3->composer(); # empty string unless found + +composer() returns the composer. By default, it gets from ID3v2 tag, +otherwise returns artist. + +You can change the inspected fields with the config() command. +Subject to normalization via C<translate_composer> or +C<translate_person> configuration variables. + +=item performer() + + $performer = $mp3->performer(); # empty string unless found + +performer() returns the main performer. By default, it gets from ID3v2 +tag C<TXXX[TPE1]>, otherwise from ID3v2 tag C<TPE1>, otherwise +returns artist. + +You can change the inspected fields with the config() command. +Subject to normalization via C<translate_performer> or +C<translate_person> configuration variables. + +=cut + +for my $elt ( qw( composer performer ) ) { + no strict 'refs'; + *$elt = sub (;$) { + my $self = shift; + my $translate = ($self->get_config("translate_$elt") + || $self->get_config("translate_person") + || [])->[0] || sub {$_[1]}; + my $fields = ($self->get_config($elt))->[0]; + return &$translate($self, $self->interpolate("%{$fields}")); + } +} + +=item config + + MP3::Tag->config(item => value1, value2...); # Set options globally + $mp3->config(item => value1, value2...); # Set object options + +When object options are first time set or get, the global options are +propagated into object options. (So if global options are changed later, these +changes are not inherited.) + +Possible items are: + +=over + +=item autoinfo + +Configure the order in which ID3v1-, ID3v2-tag and filename are used +by autoinfo. The default is C<ParseData, ID3v2, ID3v1, ImageExifTool, +CDDB_File, Inf, Cue, ImageSize, filename, LastResort>. +Options can be elements of the default list. The order +in which they are given to config also sets the order how they are +used by autoinfo. If an option is not present, it will not be used +by autoinfo (and other auto-methods if the specific overriding config +command were not issued). + + $mp3->config("autoinfo","ID3v1","ID3v2","filename"); + +sets the order to check first ID3v1, then ID3v2 and at last the +Filename + + $mp3->config("autoinfo","ID3v1","filename","ID3v2"); + +sets the order to check first ID3v1, then the Filename and last +ID3v2. As the filename will be always present ID3v2 will here +never be checked. + + $mp3->config("autoinfo","ID3v1","ID3v2"); + +sets the order to check first ID3v1, then ID3v2. The filename will +never be used. + +=item title artist album year comment track genre + +Configure the order in which ID3v1- and ID3v2-tag are used +by the corresponding methods (e.g., comment()). Options can be +the same as for C<autoinfo>. The order +in which they are given to config also sets the order how they are +used by comment(). If an option is not present, then C<autoinfo> option +will be used instead. + +=item extension + +regular expression to match the file extension (including the dot). The +default is to match 1..4 letter extensions which are not numbers. + +=item composer + +string to put into C<%{}> to interpolate to get the composer. Default +is C<'TCOM|a'>. + +=item performer + +string to put into C<%{}> to interpolate to get the main performer. +Default is C<'TXXX[TPE1]|TPE1|a'>. + +=item parse_data + +the data used by L<MP3::Tag::ParseData> handler; each option is an array +reference of the form C<[$flag, $string, $pattern1, ...]>. All the options +are processed in the following way: patterns are matched against $string +until one of them succeeds; the information obtained from later options takes +precedence over the information obtained from earlier ones. + +=item parse_split + +The regular expression to split the data when parsing with C<n> or C<l> flags. + +=item parse_filename_ignore_case + +If true (default), calling parse() and parse_rex() with match-filename +escapes (such as C<%=D>) matches case-insensitively. + +=item parse_filename_merge_dots + +If true (default), calling parse() and parse_rex() with match-filename +escapes (such as C<%=D>) does not distinguish a dot and many consequent +dots. + +=item parse_join + +string to put between multiple occurences of a tag in a parse pattern; +defaults to C<'; '>. E.g., parsing C<'1988-1992, Homer (LP)'> with pattern +C<'%c, %a (%c)'> results in comment set to C<'1988-1992; LP'> with the +default value of C<parse_join>. + +=item v2title + +Configure the elements of ID3v2-tag which are used by ID3v2::title(). +Options can be "TIT1", "TIT2", "TIT3"; the present values are combined. +If an option is not present, it will not be used by ID3v2::title(). + +=item cddb_files + +List of files to look for in the directory of MP3 file to get CDDB info. + +=item year_is_timestamp + +If TRUE (default) parse() will match complicated timestamps against C<%y>; +for example, C<2001-10-23--30,2002-02-28> is a range from 23rd to 30th of +October 2001, I<and> 28th of February of 2002. According to ISO, C<--> can +be replaced by C</> as well. For convenience, the leading 0 can be omited +from the fields which ISO requires to be 2-digit. + +=item comment_remove_date + +When extracting the date from comment fields, remove the recognized portion +even if it is human readable (e.g., C<Recorded on 2014-3-23>) if TRUE. +Current default: FALSE. + +=item default_language + +The language to use to select ID3v2 frames, and to choose C<COMM> +ID3v2 frame accessed in comment() method (default is 'XXX'; if not +C<XXX>, this should be lowercase 3-letter abbreviation according to +ISO-639-2). + +=item default_descr_c + +The description field used to choose the C<COMM> ID3v2 frame accessed +in comment() method. Defaults to C<''>. + +=item id3v2_frame_empty_ok + +When setting the individual id3v2 frames via ParseData, do not +remove the frames set to an empty string. Default 0 (empty means 'remove'). + +=item id3v2_minpadding + +Minimal padding to reserve after ID3v2 tag when writing (default 128), + +=item id3v2_sizemult + +Additionally to C<id3v2_minpadding>, insert padding to make file size multiple +of this when writing ID3v2 tag (default 512), Should be power of 2. + +=item id3v2_shrink + +If TRUE, when writing ID3v2 tag, shrink the file if needed (default FALSE). + +=item id3v2_mergepadding + +If TRUE, when writing ID3v2 tag, consider the 0-bytes following the +ID3v2 header as writable space for the tag (default FALSE). + +=item update_length + +If TRUE, when writing ID3v2 tag, create a C<TLEN> tag if the duration +is known (as it is after calling methods like C<total_secs>, or +interpolation the duration value). If this field is 2 or more, force +creation of ID3v2 tag by update_tags() if the duration is known. + +=item translate_* + +FALSE, or a subroutine used to munch a field C<*> (out of C<title +track artist album comment year genre comment_collection comment_track +title_track artist_collection person>) to some "normalized" form. +Takes two arguments: the MP3::Tag object, and the current value of the +field. + +The second argument may also have the form C<[value, handler]>, where +C<handler> is the string indentifying the handler which returned the +value. + +=item short_person + +Similar to C<translate_person>, but the intent is for this subroutine +to translate a personal name field to a shortest "normalized" form. + +=item person_frames + +list of ID3v2 frames subject to normalization via C<translate_person> +handler; current default is C<TEXT TCOM TXXX[TPE1] TPE1 TPE3 TOPE TOLY +TMCL TIPL TENC TXXX[person-file-by]>. +Used by select_id3v2_frame_by_descr(), frame_translate(), +frames_translate(). + +=item id3v2_missing_fatal + +If TRUE, interpolating ID3v2 frames (e.g., by C<%{TCOM}>) when +the ID3v2 tags is missing is a fatal error. If false (default), in such cases +interpolation results in an empty string. + +=item id3v2_recalculate + +If TRUE, interpolating the whole ID3v2 tag (by C<%{ID3v2}>) will recalculate +the tag even if its contents is not modified. + +=item parse_minmatch + +may be 0, 1, or a list of C<%>-escapes (matching any string) which should +matched non-greedily by parse() and friends. E.g., parsing +C<'Adagio - Andante - Piano Sonata'> via C<'%t - %l'> gives different results +for the settings 0 and 1; note that greediness of C<%l> does not matter, +thus the value of 1 is equivalent for the value of C<t> for this particular +pattern. + +=item id3v23_unsync_size_w + +Old experimental flag to test why ITunes refuses to handle unsyncronized tags +(does not help, see L<id3v23_unsync>). The idea was that +version 2.3 of the standard is not clear about frame size field, whether it +is the size of the frame after unsyncronization, or not. We assume +that this size is one before unsyncronization (as in v2.2). +Setting this value to 1 will assume another interpretation (as in v2.4) for +write. + +=item id3v23_unsync + +Some broken MP3 players (e.g., ITunes, at least up to v6) refuse to +handle unsyncronized (i.e., written as the standard requires it) tags; +they may need this to be set to FALSE. Default: TRUE. + +(Some details: by definition, MP3 files should contain combinations of bytes +C<FF F*> or C<FF E*> only at the start of audio frames ("syncronization" points). +ID3v2 standards take this into account, and supports storing raw tag data +in a format which does not contain these combinations of bytes +[via "unsyncronization"]. Itunes etc do not only emit broken MP3 files +[which cause severe hiccups in players which do not know how to skip ID3v2 +tags, as most settop DVD players], they also refuse to read ID3v2 tags +written in a correct, unsyncronized, format.) + +(Note also that the issue of syncronization is also applicable to ID3v1 +tags; however, since this data is near the end of the file, many players +are able to recognize that the syncronization points in ID3v1 tag cannot +start a valid frame, since there is not enough data to read; some other +players would hiccup anyway if ID3v1 contains these combinations of bytes...) + +=item encoded_v1_fits + +If FALSE (default), data containing "high bit characters" is considered to +not fit ID3v1 tag if one of the following conditions hold: + +=over 4 + +=item 1. + +C<encode_encoding_v1> is set (so the resulting ID3v1 tag is not +standard-complying, thus ambiguous without ID3v2), or + +=item 2. + +C<encode_encoding_v1> is not set, but C<decode_encoding_v1> is set +(thus read+write operation is not idempotent for ID3v1 tag). + +=back + +With the default setting, these problems are resolved as far as (re)encoding +of ID3v2 tag is non-ambiguous (which holds with the default settings for +ID3v2 encodeing). + +=item decode_encoding_v1 + +=item encode_encoding_v1 + +=item decode_encoding_v2 + +=item decode_encoding_filename + +=item decode_encoding_inf + +=item decode_encoding_cddb_file + +=item decode_encoding_cue + +=item decode_encoding_files + +=item encode_encoding_files + +Encodings of C<ID3v1>, non-Unicode frames of C<ID3v2>, filenames, +external files, F<.inf> files, C<CDDB> files, F<.cue> files, +and user-specified files correspondingly. The value of 0 means "latin1". + +The default values for C<decode_encoding_*> are set from the +corresponding C<MP3TAG_DECODE_*_DEFAULT> environment variable (here +C<*> stands for the uppercased last component of the name); if this +variable is not set, from C<MP3TAG_DECODE_DEFAULT>. Likewise, the +default value for C<encode_encoding_v1> is set from +C<MP3TAG_ENCODE_V1_DEFAULT> or C<MP3TAG_ENCODE_DEFAULT>; if not +present, from the value for C<decode_encoding_v1>; similarly for +C<encode_encoding_files>. + +Note that C<decode_encoding_v2> has no "encode" pair; it may also be disabled +per tag via effects of C<ignore_trusted_encoding0_v2> and the corresponding +frame C<TXXX[trusted_encoding0_v2]> in the tag. One should also keep in +mind that the ID3v1 standard requires the encoding to be "latin1" (so +does not store the encoding anywhere); this does not make a lot of sense, +and a lot of effort of this module is spend to fix this unfortunate flaw. +See L<"Problems with ID3 format">. + +=item ignore_trusted_encoding0_v2 + +If FALSE (default), and the frame C<TXXX[trusted_encoding0_v2]> is set to TRUE, +the setting of C<decode_encoding_v2> is ignored. + +=item id3v2_set_trusted_encoding0 + +If TRUE (default), and frames are converted from the given C<decode_encoding_v2> +to a standard-conforming encoding, a frame C<TXXX[trusted_encoding0_v2]> with +a TRUE value is added. + +[The purpose is to make multi-step update in presence of C<decode_encoding_v2> +possible; with C<id3v2_set_trusted_encoding0> TRUE, and +C<ignore_trusted_encoding0_v2> FALSE (both are default values), editing of tags +can be idempotent.] + +=item id3v2_fix_encoding_on_write + +If TRUE and C<decode_encoding_v2> is defined, the ID3v2 frames are converted +to standard-conforming encodings on write. The default is FALSE. + +=item id3v2_fix_encoding_on_edit + +If TRUE (default) and C<decode_encoding_v2> is defined (and not disabled +via a frame C<TXXX[trusted_encoding0_v2]> and the setting +C<ignore_trusted_encoding0_v2>), a CYA action is performed when an +edit may result in a confusion. More precise, adding an ID3v2 frame which +is I<essentially> affected by C<decode_encoding_v2> would convert other +frames to a standard-conforming encoding (and would set +C<TXXX[trusted_encoding0_v2]> if required by C<id3v2_set_trusted_encoding0>). + +Recall that the added frames are always encoded in standard-conformant way; +the action above avoids mixing non-standard-conformant frames with +standard-conformant frames. Such a mix could not be cleared up by setting +C<decode_encoding_v2>! One should also keep in mind that this does not affect +frames which contain characters above C<0x255>; such frames are always written +in Unicode, thus are not affected by C<decode_encoding_v2>. + +=item id3v2_frames_autofill + +Hash of suggested ID3v2 frames to autogenerate basing on extra information +available; keys are frame descriptors (such as C<TXXX[cddb_id]>), values +indicate whether ID3v2 tag should be created if it was not present. + +This variable is inspected by the method C<id3v2_frames_autofill>, +which is not called automatically when the tag is accessed, but may be called +by scripts using the module. + +The default is to force creation of tag for C<TXXX[MCDI-fulltoc]> frame, and do not +force creation for C<TXXX[cddb_id]> and C<TXXX[cdindex_id]>. + +=item local_cfg_file + +Name of configuration file read at startup by the method parse_cfg(); is +C<~>-substituted; defaults to F<~/.mp3tagprc>. + +=item prohibit_v24 + +If FALSE (default), reading of ID3v2.4 is allowed (it is not fully supported, +but most things work acceptably). + +=item write_v24 + +If FALSE (default), writing of ID3v2.4 is prohibited (it is not fully +supported; allow on your own risk). + +=item name_for_field_normalization + +interpolation of this string is used as a person name to normalize +title-like fields. Defaults to C<%{composer}>. + +=item extra_config_keys + +List of extra config keys (default is empty); setting these would not cause +warnings, and would not affect operation of C<MP3::Tag>. Applications using +this module may add to this list to allow their configuration by the same +means as configuration of C<MP3::Tag>. + +=item is_writable + +Contains a boolean value, or a method name and argument list +to call whether the tag may be added to the file. Defaults to +writable_by_extension(). + +=item writable_extensions + +Contains a list of extensions (case insensitive) for which the tag may be +added to the file. Current default is C<mp3 mp2 id3 tag ogg mpg mpeg +mp4 aiff flac ape ram mpc> (extracted from L<ExifTool> docs; may be tuned +later). + +=item * + +Later there will be probably more things to configure. + +=back + +=cut + +my $conf_rex; + +sub config { + my ($self, $item, @options) = @_; + $item = lc $item; + my $config = ref $self ? ($self->{config} ||= {%config}) : \%config; + my @known = qw(autoinfo title artist album year comment track genre + v2title cddb_files force_interpolate parse_data parse_split + composer performer default_language default_descr_c + update_length id3v2_fix_encoding_on_write + id3v2_fix_encoding_on_edit extra_config_keys + parse_join parse_filename_ignore_case encoded_v1_fits + parse_filename_merge_dots year_is_timestamp + comment_remove_date extension id3v2_missing_fatal + id3v2_frame_empty_ok id3v2_minpadding id3v2_sizemult + id3v2_shrink id3v2_mergepadding person_frames short_person + parse_minmatch id3v23_unsync id3v23_unsync_size_w + id3v2_recalculate ignore_trusted_encoding0_v2 + id3v2_set_trusted_encoding0 write_v24 prohibit_v24 + encode_encoding_files encode_encoding_v1 encode_encoding_cue + decode_encoding_v1 decode_encoding_v2 + decode_encoding_filename decode_encoding_files + decode_encoding_inf decode_encoding_cddb_file + name_for_field_normalization is_writable writable_extensions + id3v2_frames_autofill local_cfg_file); + my @tr = map "translate_$_", qw( title track artist album comment + year genre comment_collection + comment_track title_track + composer performer + artist_collection person ); + my $e_known = $self->get_config('extra_config_keys'); + $e_known = [map lc, @$e_known]; + $conf_rex = '^(' . join('|', @known, @$e_known, @tr) . ')$' unless $conf_rex; + + if ($item =~ /^(force)$/) { + return $config->{$item} = {@options}; + } elsif ($item !~ $conf_rex) { + warn "MP3::Tag::config(): Unknown option '$item' found; known options: @known @$e_known @tr\n REX = <<<$conf_rex>>>\n"; + return; + } + undef $conf_rex if $item eq 'extra_config_keys'; + + $config->{$item} = \@options; +} + +=item get_config + + $opt_array = $mp3->get_config("item"); + +When object options are first time set or get, the global options are +propagated into object options. (So if global options are changed later, these +changes are not inherited.) + +=item get_config1 + + $opt = $mp3->get_config1("item"); + +Similar to get_config(), but returns UNDEF if no config array is present, or +the first entry of array otherwise. + +=cut + +sub get_config ($$) { + my ($self, $item) = @_; + my $config = ref $self ? ($self->{config} ||= {%config}) : \%config; + $config->{lc $item}; +} + +sub get_config1 { + my $self = shift; + my $c = $self->get_config(@_); + $c and $c->[0]; +} + +=item name_for_field_normalization + + $name = $mp3->name_for_field_normalization; + +Returns "person name" to use for normalization of title-like fields; +it is the result of interpolation of the configuration variable +C<name_for_field_normalization> (defaults to C<%{composer}> - which, by +default, expands the same as C<%{TCOM|a}>). + +=cut + +sub name_for_field_normalization ($) { + my $self = shift; + $self->interpolate( $self->get_config1("name_for_field_normalization") ); +} + +=item pure_filetags + + $data = $mp3->pure_filetags()->autoinfo; + +Configures $mp3 to not read anything except the pure ID3v2 or ID3v1 tags, and +do not postprocess them. Returns the object reference itself to simplify +chaining of method calls. + +=cut + +sub pure_filetags ($) { + my $self = shift; + for my $c (qw(autoinfo title artist album year comment track genre)) { + $self->config($c,"ID3v2","ID3v1"); + } + $self->config('comment_remove_date', 0); + for my $k (%{$self->{config}}) { + delete $self->{config}->{$k} if $k =~ /^translate_/; + } + return $self; +} + +=item get_user + + $data = $mp3->get_user($n); # n-th piece of user scratch space + +Queries an entry in a scratch array ($n=3 corresponds to C<%{U3}>). + +=item set_user + + $mp3->set_user($n, $data); # n-th piece of user scratch space + +Sets an entry in a scratch array ($n=3 corresponds to C<%{U3}>). + +=cut + +sub get_user ($$) { + my ($self, $item) = @_; + unless ($self->{userdata}) { + local $self->{__proxy}[0] = $self unless $self->{__proxy}[0] or $ENV{MP3TAG_TEST_WEAKEN}; + $self->{ParseData}->parse('track'); # Populate the hash if possible + $self->{userdata} ||= []; + } + return unless defined (my $d = $self->{userdata}[$item]); + $d; +} + +sub set_user ($$$) { + my ($self, $item, $val) = @_; + $self->{userdata} ||= []; + $self->{userdata}[$item] = $val; +} + +=item set_id3v2_frame + + $mp3->set_id3v2_frame($name, @values); + +When called with only $name as the argument, removes the specified +frame (if it existed). Otherwise sets the frame passing the specified +@values to the add_frame() function of MP3::Tag::ID3v2. (The old value is +removed.) + +=cut + +# With two elements, removes frame +sub set_id3v2_frame ($$;@) { + my ($self, $item) = (shift, shift); + $self->get_tags; + return if not @_ and not exists $self->{ID3v2}; + $self->new_tag("ID3v2") unless exists $self->{ID3v2}; + $self->{ID3v2}->remove_frame($item) + if defined $self->{ID3v2}->get_frame($item); + return unless @_; + return $self->{ID3v2}->add_frame($item, @_); +} + +=item get_id3v2_frames + + ($descr, @frames) = $mp3->get_id3v2_frames($fname); + +Returns the specified frame(s); has the same API as +L<MP3::Tag::ID3v2::get_frames>, but also returns undef if no ID3v2 +tag is present. + +=cut + +sub get_id3v2_frames ($$;$) { + my ($self) = (shift); + $self->get_tags; + return if not exists $self->{ID3v2}; + $self->{ID3v2}->get_frames(@_); +} + +=item delete_tag + + $deleted = $mp3->delete_tag($tag); + +$tag should be either C<ID3v1> or C<ID3v2>. Deletes the tag if it is present. +Returns FALSE if the tag is not present. + +=cut + +sub delete_tag ($$) { + my ($self, $tag) = (shift, shift); + $self->get_tags; + die "Unexpected tag type '$tag'" unless $tag =~ /^ID3v[12]$/; + return unless exists $self->{$tag}; + my $res = $self->{$tag}->remove_tag(); + $res = ($res >= 0) if $tag eq 'ID3v1'; # -1 on error + $res or die "Error deleting tag `$tag'"; +} + +=item is_id3v2_modified + + $frame = $mp3->is_id3v2_modified(); + +Returns TRUE if ID3v2 tag exists and was modified after creation. + +=cut + +sub is_id3v2_modified ($$;@) { + my ($self) = (shift); + return if not exists $self->{ID3v2}; + $self->{ID3v2}->is_modified(); +} + +=item select_id3v2_frame + + $frame = $mp3->select_id3v2_frame($fname, $descrs, $langs [, $VALUE]); + +Returns the specified frame(s); has the same API as +L<MP3::Tag::ID3v2/frame_select> (args are the frame name, the list of +wanted Descriptors, list of wanted Languages, and possibly the new +contents - with C<undef> meaning deletion). For read-only access it +returns empty if no ID3v2 tag is present, or no frame is found. + +If new contents is specified, B<ALL> the existing frames matching the +specification are deleted. + +=item have_id3v2_frame + + $have_it = $mp3->have_id3v2_frame($fname, $descrs, $langs); + +Returns TRUE the specified frame(s) exist; has the same API as +L<MP3::Tag::ID3v2::frame_have> (args are frame name, list of wanted +Descriptors, list of wanted Languages). + +=item get_id3v2_frame_ids + + $h = $mp3->get_id3v2_frame_ids(); + print " $_ => $h{$_}" for keys %$h; + +Returns a hash reference with the short names of ID3v2 frames present +in the tag as keys (and long description of the meaning as values), or +FALSE if no ID3v2 tag is present. See +L<MP3::Tags::ID3v2::get_frame_ids> for details. + +=item id3v2_frame_descriptors + +Returns the list of human-readable "long names" of frames (such as +C<COMM(eng)[lyricist birthdate]>), appropriate for interpolation, or +for select_id3v2_frame_by_descr(). + +=item select_id3v2_frame_by_descr + +=item have_id3v2_frame_by_descr + +Similar to select_id3v2_frame(), have_id3v2_frame(), but instead of +arguments $fname, $descrs, $langs take one string of the form + + NAME(langs)[descr] + +Both C<(langs)> and C<[descr]> parts may be omitted; langs should +contain comma-separated list of needed languages. The semantic is +similar to +L<MP3::Tag::ID3v2::frame_select_by_descr_simpler|MP3::Tag::ID3v2/frame_select_by_descr_simpler>. + +It is allowed to have C<NAME> of the form C<FRAMnn>; C<nn>-th frame +with name C<FRAM> is chosen (C<-1>-based: the first frame is C<FRAM>, +the second C<FRAM00>, the third C<FRAM01> etc; for more user-friendly +scheme, use C<langs> of the form C<#NNN>, with C<NNN> 0-based; see +L<MP3::Tag::ID3v2/"get_frame_ids()">). + + $frame = $mp3->select_id3v2_frame_by_descr($descr [, $VALUE1, ...]); + $have_it = $mp3->have_id3v2_frame_by_descr($descr); + +select_id3v2_frame_by_descr() will also apply the normalizer in config +setting C<translate_person> if the frame name matches one of the +elements of the configuration setting C<person_frames>. + + $c = $mp3->select_id3v2_frame_by_descr("COMM(fre,fra,eng,#0)[]"); + $t = $mp3->select_id3v2_frame_by_descr("TIT2"); + $mp3->select_id3v2_frame_by_descr("TIT2", "MyT"); # Set/Change + $mp3->select_id3v2_frame_by_descr("RBUF", $n1, $n2, $n3); # Set/Change + $mp3->select_id3v2_frame_by_descr("RBUF", "$n1;$n2;$n3"); # Set/Change + $mp3->select_id3v2_frame_by_descr("TIT2", undef); # Remove + +Remember that when select_id3v2_frame_by_descr() is used for +modification, B<ALL> found frames are deleted before a new one is +added. For gory details, see L<MP3::Tag::ID3v2/frame_select>. + +=item frame_translate + + $mp3->frame_translate('TCOM'); # Normalize TCOM ID3v2 frame + +assuming that the frame value denotes a person, normalizes the value +using personal name normalization logic (via C<translate_person> +configuration value). Frame is updated, but the tag is not written +back. The frame must be in the list of personal names frames +(C<person_frames> configuration value). + +=item frames_translate + +Similar to frame_translate(), but updates all the frames in +C<person_frames> configuration value. + +=cut + +sub select_id3v2_frame ($$;@) { + my ($self) = (shift); + $self->get_tags; + if (not exists $self->{ID3v2}) { + return if @_ <= 3 or not defined $_[3]; # Read access, or deletion + $self->new_tag("ID3v2"); + } + $self->{ID3v2}->frame_select(@_); +} + +sub _select_id3v2_frame_by_descr ($$$;@) { + my ($self, $update) = (shift, shift); + $self->get_tags; + if (not exists $self->{ID3v2}) { + return if @_ <= 1 or @_ <= 2 and not defined $_[1]; # Read or delete + $self->new_tag("ID3v2"); + } + my $fname = $_[0]; + $fname =~ s/^(\w{4})\d+/$1/; # if FRAMnn, convert to FRAM + my $tr = ($self->get_config('translate_person') || [])->[0]; + if ($tr) { + my $translate = $self->get_config('person_frames'); + unless (ref $translate eq 'HASH') { # XXXX Store the hash somewhere??? + $translate = {map +($_, 1), @$translate}; + #$self->config('person_frames', @translate); + } + my $do = $translate->{$fname}; + $do = $translate->{$fname} # Remove language + if not $do and $fname =~ s/^(\w{4})(?:\(([^()]*(?:\([^()]+\)[^()]*)*)\))/$1/; + undef $tr unless $do; + } + return if $update and not $tr; + $tr ||= sub {$_[1]}; + return $self->{ID3v2}->frame_select_by_descr_simpler(@_) + if @_ > 2 or @_ == 2 and not defined $_[1]; # Multi-arg write or delete + return $self->{ID3v2}->frame_select_by_descr_simpler( + $_[0], &$tr($self, $_[1]) + ) if @_ == 2; # Write access with one arg + + my $val = $self->{ID3v2}->frame_select_by_descr_simpler(@_); + my $nval; + $nval = &$tr($self, $val) if defined $val; + return $nval unless $update; + # Update logic: + return if not defined $val or $val eq $nval; + $self->{ID3v2}->frame_select_by_descr_simpler($_[0], $nval); +} + +sub select_id3v2_frame_by_descr ($$;@) { + my ($self) = (shift); + return $self->_select_id3v2_frame_by_descr(0, @_); +} + +sub frame_translate ($@) { + my ($self) = (shift); + return $self->_select_id3v2_frame_by_descr(1, @_); +} + +sub frames_translate ($) { + my ($self) = (shift); + for my $f (@{$self->get_config('person_frames') || []}) { + $self->frame_translate($f); + } +} + +sub have_id3v2_frame ($$;@) { + my ($self) = (shift); + $self->get_tags; + return if not exists $self->{ID3v2}; + $self->{ID3v2}->frame_have(@_); +} + +sub have_id3v2_frame_by_descr ($$) { + my ($self) = (shift); + $self->get_tags; + return if not exists $self->{ID3v2}; + $self->{ID3v2}->frame_have_by_descr(shift); +} + +sub get_id3v2_frame_ids ($$) { + my ($self) = (shift); + $self->get_tags; + return if not exists $self->{ID3v2}; + $self->{ID3v2}->get_frame_ids(@_); +} + +sub id3v2_frame_descriptors ($) { + my ($self) = (shift); + $self->get_tags; + return if not exists $self->{ID3v2}; + $self->{ID3v2}->get_frame_descriptors(@_); +} + +=item copy_id3v2_frames($from, $to, $overwrite, [$keep_flags, $f_ids]) + +Copies specified frames between C<MP3::Tag> objects $from, $to. Unless +$keep_flags, the copied frames have their flags cleared. +If the array reference $f_ids is not specified, all the frames (but C<GRID> +and C<TLEN>) are considered (subject to $overwrite), otherwise $f_ids should +contain short frame ids to consider. Group ID flag is always cleared. + +If $overwrite is C<'delete'>, frames with the same descriptors (as +returned by get_frame_descr()) in $to are deleted first, then all the +specified frames are copied. If $overwrite is FALSE, only frames with +descriptors not present in $to are copied. (If one of these two +conditions is not met, the result may be not conformant to standards.) + +Returns count of copied frames. + +=cut + +sub copy_id3v2_frames { + my ($from, $to, $overwrite, $keep_flags, $f_ids) = @_; + $from->get_tags; + return 0 unless $from = $from->{ID3v2}; # No need to create it... + $f_ids ||= [keys %{$from->get_frame_ids}]; + return 0 unless @$f_ids; + $to->get_tags; + $to->new_tag("ID3v2") if not exists $to->{ID3v2}; + $from->copy_frames($to->{ID3v2}, $overwrite, $keep_flags, $f_ids); +} + +sub _Data_to_MIME ($$;$) { + goto &MP3::Tag::ID3v2::_Data_to_MIME +} + +=item _Data_to_MIME + +Internal method to extract MIME type from a string the image file content. +Returns C<application/octet-stream> for unrecognized data +(unless extra TRUE argument is given). + + $format = $id3->_Data_to_MIME($data); + +Currently, only the first 4 bytes of the string are inspected. + +=cut + + +=item shorten_person + + $string = $mp3->shorten_person($person_name); + +shorten $person_name as a personal name (according to C<short_person> +configuration setting). + +=cut + +sub shorten_person ($$) { + my $self = shift; + my $tr = ($self->get_config('short_person') || [])->[0]; + return shift unless $tr; + return &$tr($self, shift); +} + +=item normalize_person + + $string = $mp3->normalize_person($person_name); + +normalize $person_name as a personal name (according to C<translate_person> +configuration setting). + +=cut + +sub normalize_person ($$) { + my $self = shift; + my $tr = ($self->get_config('translate_person') || [])->[0]; + return shift unless $tr; + return &$tr($self, shift); +} + + +=item id3v2_frames_autofill + + $mp3->id3v2_frames_autofill($force); + +Generates missing tags from the list specified in C<id3v2_frames_autofill> +configuration variable. The tags should be from a short list this method +knows how to deal with: + + TXXX[MCDI-fulltoc]: filled from file audio_cd.toc in directory of the + audio file. [Create this file with + readcd -fulltoc dev=0,1,0 -f=audio_cd >& nul + modifying the dev (and redirection per your shell). ] + TXXX[cddb_id] + TXXX[cdindex_id]: filled from the result of the corresponding method + (which may extract from .inf or cddb files). + +Existing frames are not modified unless $force option is specified; when +$force is true, ID3v2 tag will be created even if it was not present. + +=cut + +sub id3v2_frames_autofill ($$) { + my ($self, $forceframe) = (shift, shift); + my %force = @{$self->get_config('id3v2_frames_autofill')}; + $self->get_tags; + unless ($self->{ID3v2} or $forceframe) { # first run: force ID3v2? + for my $tag (keys %force) { + next unless $force{$tag}; + my $v; + $v = $self->$1() or next if $tag =~ /^TXXX\[(cd(?:db|index)_id)\]$/; + if ($tag eq 'TXXX[MCDI-fulltoc]') { + my $file = $self->interpolate('%D/audio_cd.toc'); + $v = -e $file; + } + $forceframe = 1, last if $v + } + } + for my $tag (keys %force) { + next if $self->have_id3v2_frame_by_descr($tag) and not $forceframe; + next unless $force{$tag} or $self->{ID3v2} or $forceframe; + my $v; + $v = $self->$1() or next if $tag =~ /^TXXX\[(cd(?:db|index)_id)\]$/; + if ($tag eq 'TXXX[MCDI-fulltoc]') { + my $file = $self->interpolate('%D/audio_cd.toc'); + next unless -e $file; + warn(<<EOW), next unless $self->track; +Could deduce MCDI info, but per id3v2.4 specs, must know the track number! +EOW + eval { + open F, "< $file" or die "Can't open `$file' for read: $!"; + binmode F or die "Can't binmode `$file' for read: $!"; + local $/; + $v = <F>; + CORE::close F or die "Can't close `$file' for read: $!"; + } or warn($@), next; + } + $self->select_id3v2_frame_by_descr($tag, $v), next if defined $v; + die "id3v2_frames_autofill(): do not know how to create frame `$tag'"; + } +} + +=item interpolate + + $string = $mp3->interpolate($pattern) + +interpolates C<%>-escapes in $pattern using the information from $mp3 tags. +The syntax of escapes is similar to this of sprintf(): + + % [ [FLAGS] MINWIDTH] [.MAXWIDTH] ESCAPE + +The only recognized FLAGS are C<-> (to denote left-alignment inside MINWIDTH- +wide field), C<' '> (SPACE), and C<0> (denoting the fill character to use), as +well as an arbitrary character in parentheses (which becomes the fill +character). MINWIDTH and MAXWIDTH should be numbers. + +The short ESCAPEs are replaced by + + % => literal '%' + t => title + a => artist + l => album + y => year + g => genre + c => comment + n => track + f => filename without the directory path + F => filename with the directory path + D => the directory path of the filename + E => file extension + e => file extension without the leading dot + A => absolute filename without extension + B => filename without the directory part and extension + N => filename as originally given without extension + + v mpeg_version + L mpeg_layer_roman + r bitrate_kbps + q frequency_kHz + Q frequency_Hz + S total_secs_int + M total_millisecs_int + m total_mins + mL leftover_mins + H total_hours + s leftover_secs + SL leftover_secs_trunc + ML leftover_msec + SML leftover_secs_float + C is_copyrighted_YN + p frames_padded_YN + o channel_mode + u frames + + h height (these 3 for image files, Image::Size or Image::ExifData required) + w width + iT img_type + mT mime_type + mP mime_Pretype (the capitalized first part of mime_type) + aR aspect_ratio (width/height) + a3 aspect_ratio3 (3 decimal places after the dot) + aI aspect_ratio_inverted (height/width) + bD bit_depth + + aC collection artist (from CDDB_File) + tT track title (from CDDB_File) + cC collection comment (from CDDB_File) + cT track comment (from CDDB_File) + iC CDDB id + iI CDIndex id + +(Multi-char escapes must be inclosed in braces, as in C<%{SML}> or C<%.5{aR}>. + +Additional multi-char escapes are interpretated is follows: + +=over 4 + +=item * + +Names of ID3v2 frames are replaced by their text values (empty for missing +frames). + +=item * + +Strings C<n1> and C<n2> are replaced by "pure track number" and +"max track number" (this allows for both formats C<N1> and C<N1/N2> of "track", +the latter meaning track N1 of N2); use C<n0> to pad C<n1> with leading 0 +to the width of C<n2> (in absense of n2, to 2). Likewise for C<m1>, C<m2> +but with disk (media) number instead of track number; use C<mA> to encode +C<m1> as a letter (see L<disk_alphanum()>). + +=item * + +Strings C<ID3v1> and C<ID3v2> are replaced by the whole ID3v1/2 tag +(interpolation of C<ID3v2> for an unmodified tag is subject to +C<id3v2_recalculate> configuration variable). (These may work as +conditionals too, with C<:>.) + +=item * + +Strings of the form C<FRAM(list,of,languages)[description]'> are +replaced by the first FRAM frame with the descriptor "description" in +the specified comma-separated list of languages. Instead of a +language (ID3v2 uses lowercase 3-char ISO-639-2 language notations) one can use +a string of the form C<#Number>; e.g., C<#4> means 4th FRAM frame, or +FRAM04. Empty string for the language means any language.) Works as +a condition for conditional interpolation too. + +Any one of the list of languages and the disription can be omitted; +this means that either the frame FRAM has no language or descriptor +associated, or no restriction should be applied. + +Unknown language should be denoted as C<XXX> (in uppercase!). The language +match is case-insensitive. + +=item * + +Several descriptors of the form +C<FRAM(list,of,languages)[description]'> discussed above may be +combined together with C<&>; the non-empty expansions are joined +together with C<"; ">. Example: + + %{TXXX[pre-title]&TIT1&TIT2&TIT3&TXXX[post-title]} + + +=item * + +C<d>I<NUMBER> is replaced by I<NUMBER>-th component of the directory name (with +0 corresponding to the last component). + +=item * + +C<D>I<NUMBER> is replaced by the directory name with NUMBER components stripped. + +=item * + +C<U>I<NUMBER> is replaced by I<NUMBER>-th component of the user scratch +array. + +=item * + +If string starts with C<FNAME:>: if frame FNAME does not exists, the escape +is ignored; otherwise the rest of the string is reinterpreted. + +=item * + +String starting with C<!FNAME:> are treated similarly with inverted test. + +=item * + +If string starts with C<FNAME||>: if frame FNAME exists, the part +after C<||> is ignored; otherwise the part before C<||> is ignored, +and the rest is reinterpreted. + +=item * + +If string starts with C<FNAME|>: if frame FNAME exists, the part +after C<|> is ignored; otherwise the part before C<|> is ignored, +and the rest is reinterpreted as if it started with C<%{>. + +=item * + +String starting with I<LETTER>C<:> or C<!>I<LETTER>C<:> are treated similarly +to ID3v2 conditionals, but the condition is that the corresponding escape +expands to non-empty string. Same applies to non-time related 2-char escapes +and user variables. + +=item * + +Likewise for string starting with I<LETTER>C<|> or I<LETTER>C<||>. + +=item * + +For strings of the form C<nmP[VALUE]> or C<shP[VALUE]>, I<VALUE> is +interpolated, then normalized or shortened as a personal name +(according to C<translate_person> or C<short_person> configuration +setting). + +=item * + +C<composer> or C<performer> is replaced by the result of calling the +corresponding method. + +=item * + +C<frames> is replaced by space-separated list of "long names" of ID3v2 +frames (see id3v2_frame_descriptors()). (To use a different separator, +put it after slash, as in %{frames/, }, where separator is COMMA +SPACE). + +=item * + +C<_out_frames[QQPRE//QQPOST]> is replaced by a verbose listing of frames. +"simple" frames are output one-per-line (with the value surrounded by +C<QQPRE> and C<QQPOST>); fields of other frames are output one-per-line. +If one omits the leading C<_>, then C<__binary_DATA__> replaces the value +of binary fields. + +=item * + +C<ID3v2-size>, C<ID3v2-pad>, and C<ID3v2-stripped> are replaced by size of +ID3v2 tag in bytes, the amount of 0-padding at the end of the tag +(not counting one extra 0 byte at the end of tag which may be needed for +unsyncing if the last char is \xFF), and size without padding. Currently, +for modified ID3v2 tag, what is returned reflect the size on disk (i.e., +before modification). + +=item * + +C<ID3v2-modified> is replaced by C<'modified'> if ID3v2 is present and +is modified in memory; otherwise is replaced by an empty string. + +=item * + +For strings of the form C<I(FLAGS)VALUE>, I<VALUE> is interpolated +with flags in I<FLAGS> (see L<"interpolate_with_flags">). If FLAGS +does not contain C<i>, VALUE should have C<{}> and C<\> backwacked. + +=item * + +For strings of the form C<T[FORMAT]>, I<FORMAT> is split on comma, and +the resulting list of formats is used to convert the duration of the +audio to a string using the method format_time(). (E.g., +C<%{T[=E<gt>m,?H:,{mL}]}> would print duration in (optional) hours and minutes +rounded to the closest minute.) + +=back + +The default for the fill character is SPACE. Fill character should preceed +C<-> if both are given. Example: + + Title: %(/)-12.12t%{TIT3:; TIT3 is %\{TIT3\}}%{!TIT3:. No TIT3 is present} + +will result in + + Title: TITLE///////; TIT3 is Op. 16 + +if title is C<TITLE>, and TIT3 is C<Op. 16>, and + + Title: TITLE///////. No TIT3 is present + +if title is C<TITLE>, but TIT3 is not present. + + Fat content: %{COMM(eng,fra,fre,rus,)[FatContent]} + +will print the comment field with I<Description> C<FatContent> +prefering the description in English to one in French, Russian, or any +other language (in this order). (I do not know which one of +terminology/bibliography codes for French is used, so for safety +include both.) + + Composer: %{TCOM|a} + +will use the ID3v2 field C<TCOM> if present, otherwise uses C<%a> (this is +similar to + + Composer: %{composer} + +but the latter may be subject to (different) normalization, and/or +configuration variables). + +Interpolation of ID3v2 frames uses the minimal possible non-ambiguous +backslashing rules: the only backslashes needed are to protect the +innermost closing delimiter (C<]> or C<}>) appearing as a literal +character, or to protect backslashes I<immediately> preceding such +literal, or the closing delimiter. E.g., the pattern equal to + + %{COMM(eng)[a\b\\c\}\]end\\\]\\\\]: comment `a\b\\c\\\}]end\]\\' present} + +checks for the presence of comment with the descriptor C<a\b\\c\}]end\]\\>. +Note that if you want to write this string as a Perl literal, a lot of +extra backslashes may be needed (unless you use C<E<lt>E<lt>'FOO'> +HERE-document). + + %{T[?Hh,?{mL}m,{SML}s]} + +for a file of duration 2345.62sec will result in C<39m05.62s>, while + + %{T[?H:,?{mL}:,{SL},?{ML}]}sec + +will result in C<39:05.620sec>. + +=cut + +my %trans = qw( t title + a artist + l album + y year + g genre + c comment + n track + + E filename_extension + e filename_extension_nodot + A abs_filename_noextension + B filename_nodir_noextension + N filename_noextension + f filename_nodir + D dirname + F abs_filename + + aC artist_collection + tT title_track + cC comment_collection + cT comment_track + iD cddb_id + iI cdindex_id + n1 track1 + n2 track2 + n0 track0 + mA disk_alphanum + m1 disk1 + m2 disk2 + + h height + w width + iT img_type + mT mime_type + mP mime_Pretype + aR aspect_ratio + a3 aspect_ratio3 + aI aspect_ratio_inverted + bD bit_depth + + v mpeg_version + L mpeg_layer_roman + ? is_stereo + ? is_vbr + r bitrate_kbps + q frequency_kHz + Q frequency_Hz + ? size_bytes + S total_secs_int + M total_millisecs_int + m total_mins + mL leftover_mins + H total_hours + s leftover_secs + ML leftover_msec + SML leftover_secs_float + SL leftover_secs_trunc + ? time_mm_ss + C is_copyrighted_YN + p frames_padded_YN + o channel_mode + u frames + ? frame_len + ? vbr_scale + ); + +# Different: %v is without trailing 0s, %q has fractional part, +# %e, %E are for the extension, +# %r is a number instead of 'Variable', %u is one less... +# Missing: +# %b Number of corrupt audio frames (integer) +# %e Emphasis (string) +# %E CRC Error protection (string) +# %O Original material flag (string) +# %G Musical genre (integer) + +my $frame_bra = # FRAM | FRAM03 | FRAM(lang)[ + qr{\w{4}(?:(?:\d\d)|(?:\([^()]*(?:\([^()]+\)[^()]*)*\))?(?:(\[)|(?=[\}:|&])))}s; # 1 group for begin-descr +# used with offset by 1: 2: fill, 3: same, 4: $left, 5..6 width, 7: key +my $pat_rx = qr/^%(?:(?:\((.)\)|([^-.1-9%a-zA-Z]))?(-)?(\d+))?(?:\.(\d+))?([talgcynfFeEABDNvLrqQSmsCpouMHwh{%])/s; +# XXXX Partially repeated below, search for `talgc'??? vLrqQSmsCpouMH miss??? + +my $longer_f = qr(a[3CRI]|tT|c[TC]|i[DIT]|n[012]|m[A12TP]|bD); +# (a[CR]|tT|c[TC]|[mMS]L|SML|i[DIT]|n[012]|m[A12T]|bD) +# a[CR]|tT|c[TC]|i[DIT]|n[012]|m[A12T]|bD + +# $upto TRUE: parse the part including $upto char +# Very restricted backslashitis: only $upto and \ before $upto-or-end +# $upto defined but FALSE: interpolate only one %-escape. +# Anyway: $_[1] is modified to remove interpolated part. +sub _interpolate ($$;$$) { + # goto &interpolate_flags if @_ == 3; + my ($self, undef, $upto, $skip) = @_; # pattern is modified, so is $_[1] + $self->get_tags(); + my $res = ""; + my $ids; + die "upto=`$upto' not supported" if $upto and $upto ne ']' and $upto ne'}'; + die "upto=`$upto' not supported with skip" + if $upto and not defined $upto and $skip; + my $cnt = ($upto or not defined $upto) ? -1 : 1; # upto eq '': 1 escape + + while ($cnt-- and ($upto # undef and '' use the same code + ? ($upto eq ']' + ? $_[1] =~ s/^((?:[^%\\\]]|(?:\\\\)*\\\]|\\+[^\\\]]|\\\\)+)|$pat_rx//so + : $_[1] =~ s/^((?:[^%\\\}]|(?:\\\\)*\\\}|\\+[^\\\}]|\\\\)+)|$pat_rx//so) + : $_[1] =~ s/^([^%]+)|$pat_rx//so)) { + if (defined $1) { + my $str = $1; + if ($upto and $upto eq ']') { + $str =~ s<((?:\\\\)*)(?:\\(?=\])|(?!.))>< '\\' x (length($1)/2) >ges; + } elsif ($upto and $upto eq '}') { + $str =~ s<((?:\\\\)*)(?:\\(?=\})|(?!.))>< '\\' x (length($1)/2) >ges; + } + $res .= $str, next; + } + my ($fill, $left, $minwidth, $maxwidth, $what) + = ((defined $2 ? $2 : $3), $4, $5, $6, $7); + next if $skip and $what ne '{'; + my $str; + if ($what eq '{' and $_[1] =~ s/^([dD])(\d+)}//) { # Directory + next if $skip; + if ($1 eq 'd') { + $str = $self->dir_component($2); + } else { + $str = $self->dirname($2); + } + } elsif ($what eq '{' and $_[1] =~ s/^U(\d+)}//) { # User data + next if $skip; + $str = $self->get_user($1); + } elsif ($what eq '{' and $_[1] =~ s/^($longer_f|[mMS]L|SML)}//o) { + # CDDB, IDs, or leftover times + next if $skip; + my $meth = $trans{$1}; + $str = $self->$meth(); + } elsif ($what eq '{' and # $frame_bra has 1 group, No. 5 + # 2-char fields as above, except for [mMS]L|SML (XXX: vLrqQSmsCpouMH ???) + $_[1] =~ s/^(!)?(([talgcynfFeEABDNvLrqQSmsCpouMHwh]|ID3v[12]|ID3v2-modified|$longer_f|U\d+)(:|\|\|?)|$frame_bra)//o) { + # Alternation with simple/complicated stuff + my ($neg, $id, $simple, $delim) = ($1, $2, $3, $4); + if ($delim) { # Not a frame id... + $id = $simple; + } else { # Frame: maybe trailed by :, |, ||, maybe not + $id .= ($self->_interpolate($_[1], ']', $skip) . ']') if $5; + $_[1] =~ s/^(:|\|\|?)// and $delim = $1; + unless ($delim) { + die "Can't parse negated conditional: I see `$_[1]'" if $neg; + my $nonesuch = 0; + unless ($self->{ID3v2} or $neg) { + die "No ID3v2 present" + if $self->get_config('id3v2_missing_fatal'); + $nonesuch = 1; + } + if ($_[1] =~ s/^}//) { # frame with optional (lang)/[descr] + next if $skip or $nonesuch; + $str = $self->select_id3v2_frame_by_descr($id); + #$str = $str->{_Data} if $str and ref $str and exists $str->{_Data}; + } elsif ($_[1] =~ /^&/o) { + # join of frames with optional (language)/[descriptor] + my @id = $id; + while ($_[1] =~ s/^&($frame_bra)//o) { + $id = $1; + $id .= ($self->_interpolate($_[1], ']', $skip) . ']') if $2; + next if $skip or $nonesuch; + push @id, $id; + } + die "Can't parse &-list; I see `$_[1]'" unless $_[1] =~ s/^}//; + next if $skip or $nonesuch; + my @out; + for my $in (@id) { + $in = $self->select_id3v2_frame_by_descr($in); + #$in = $in->{_Data} if $in and ref $in and exists $in->{_Data}; + push @out, $in if defined $in and length $in; + } + $str = join '; ', @out; + } else { + die "unknown frame terminator; I see `$_[1]'"; + } + } + } + if ($delim) { # Conditional + # $self->_interpolate($_[1], $upto, $skip), next if $skip; + my $alt = ($delim ne ':') && $delim; # FALSE or $delim + die "Negation and alternation incompatible in interpolation" + if $alt and $neg; + my $have; + if ($simple and (2 >= length $simple or $simple =~ /^U/)) { + my $s = (1 == length $simple ? $simple : "{$simple}"); + $str = $self->interpolate("%$s"); + $have = length($str); + } elsif (($simple || '') eq 'ID3v2-modified') { # may be undef + $have = ${ $self->{ID3v2} || {} }{modified} || ''; + } elsif ($simple) { # ID3v2 or ID3v1 + die "ID3v2 or ID3v1 as conditionals incompatible with $alt" + if $alt; + $have = !! $self->{$simple}; # Make logical + } else { + $have = $self->have_id3v2_frame_by_descr($id); + } + my $skipping = $skip || (not $alt and $1 ? $have : !$have); + my $s; + if ($alt and $alt ne '||') { # Need to prepend % + if ($_[1] =~ s/^([^\\])}//) { # One-char escape + $s = $self->interpolate("%$1") unless $skipping; + } else { # Understood with {}; prepend %{ + $_[1] =~ s/^/%\{/ or die; + $s = $self->_interpolate($_[1], '', $skipping); + } + } else { + $s = $self->_interpolate($_[1], '}', $skipping); + } + next if $skipping; + $str = $self->select_id3v2_frame_by_descr($id) + if $alt and $have and not $simple; + $str = $s unless $have and $alt; + $str = $str->{_Data} + if $str and ref $str and exists $str->{_Data}; + } + } elsif ($what eq '{' and $_[1] =~ s/^ID3v1}//) { + next if $skip; + $str = $self->{ID3v1}->as_bin if $self->{ID3v1}; + } elsif ($what eq '{' + and $_[1] =~ s/^(sh|nm)P\[//s) { + # (Short) personal name + $what = $1; + $str = $self->_interpolate($_[1], ']', $skip); + $_[1] =~ s/^\}// or die "Can't find end of ${what}P escape; I see `$_[1]'"; + next if $skip; + my $meth = ($what eq 'sh' ? 'shorten_person' : 'normalize_person'); + $str = $self->$meth($str); + } elsif ($what eq '{' and $_[1] =~ s/^I\((\w+)\)//s) { + # Interpolate + my $flags = $1; + if ($flags =~ s/i//) { + $str = $self->_interpolate($_[1], '}', $skip); + } else { + $_[1] =~ s/^((?:[^\\\}]|(?:\\\\)*\\\}|\\+[^\\\}]|\\\\)*)\}//s + # $_[1] =~ s/^((?:\\.|[^{}\\])*)}// + or die "Can't find non-interpolated argument in `$_[1]'"; + next if $skip; + # ($str = $1) =~ s/\\([\\{}])/$1/g; + ($str = $1) =~ s<((?:\\\\)*)(?:\\(?=\})|(?!.))>< '\\' x (length($1)/2) >ges; + } + next if $skip; + ($str) = $self->interpolate_with_flags($str, $flags); + } elsif ($what eq '{' and $_[1] =~ s/^T\[([^\[\]]*)\]\}//s) { # time + next if $skip; + $str = $self->format_time(undef, split /,/, $1); + } elsif ($what eq '{') { #id3v2=whole, composer/performer/frames + unless ($self->{ID3v2} or $skip) { + die "No ID3v2 present" + if $self->get_config('id3v2_missing_fatal'); + $_[1] =~ s/^[^\}]*}//; # XXXX No error checking here... + next; + } + if ($_[1] =~ s/ID3v2}//) { # Whole tag + if (not $skip and $self->{ID3v2}) { + if ($self->get_config('id3v2_recalculate')) { + $str = $self->{ID3v2}->as_bin; + } else { + $str = $self->{ID3v2}->as_bin_raw; + } + } + } elsif ($_[1] =~ s/^(composer|performer)}//) { + $str = $self->$1() unless $skip; + } elsif ($_[1] =~ s,^frames(?:/(.*?))?},,) { + my $sep = (defined $1 ? $1 : ' '); + $str = join $sep, $self->id3v2_frame_descriptors() unless $skip; + } elsif ($_[1] =~ s,^(_)?out_frames\[(.*?)//(.*?)\]},,) { + my($bin, $pre, $post) = ($1, $2, $3); + my $v2 = $self->{ID3v2}; + # $fr_sep, $fn_sep, $pre,$post,$fsep,$pre_mult,$val_sep # length "Picture Type" = 12 + $str = ($v2 ? $v2->__frames_as_printable("\n", "\t==>\n ", $pre, # Tune tabbing for length=5..12 (_Data) + $post, "\n ", "", " \t=\t", $bin) : '') unless $skip; + } elsif ($_[1] =~ s,^ID3v2-size},,) { + my $v2 = $self->{ID3v2}; + $str = ($v2 ? 10 + $v2->{buggy_padding_size} + $v2->{tagsize} : 0) + unless $skip; + } elsif ($_[1] =~ s,^ID3v2-pad},,) { + my $v2 = $self->{ID3v2}; + $v2->get_frame_ids() if $v2 and not exists $v2->{frameIDs}; + $str = ($v2 ? $v2->{padding} : 0) unless $skip; + } elsif ($_[1] =~ s,^ID3v2-stripped},,) { + my $v2 = $self->{ID3v2}; + $v2->get_frame_ids() if $v2 and not exists $v2->{frameIDs}; + $str = ($v2 ? 10 + $v2->{buggy_padding_size} + $v2->{tagsize} - $v2->{padding} : 0) unless $skip; + } elsif ($_[1] =~ s,^ID3v2-modified},,) { + my $v2 = $self->{ID3v2}; + $str = ($v2 and $v2->{modified}) || '' unless $skip; + } else { + die "unknown escape; I see `$_[1]'"; + } + } elsif ($what eq '%') { + $str = '%'; + } else { + my $meth = $trans{$what}; + $str = $self->$meth(); + } + $str = '' unless defined $str; + if (defined $maxwidth and length $str > $maxwidth) { + if ($str =~ /^(?:\+|(\-))?(\d*)(\.\d*)?$/) { + if (length($1 || '') + length $2 <= $maxwidth) { + my $w = $maxwidth - length $2 - length($1 || ''); + $w-- if $w; # Take into account decimal point... + $str = sprintf '%.*f', $w, $str + } else { # Might be a long integer benefiting from %g + my($w, $s0) = ($maxwidth, $str); + while ($w >= 1) { + $str = sprintf '%.*g', $w, $s0; + $str =~ s/(^|(?<=[-+]))0+|(?<=e)\+//gi; # 1e+07 to 1e7 + last if length $str <= $maxwidth; + $w-- + } + $str = $s0 if length $str > length $s0; # 12 vs 1e1 + $str = substr $str, 0, $maxwidth; # 1e as a truncation of 1234 is better than 12... + } + } else { + $str = substr $str, 0, $maxwidth; + } + } + if (defined $minwidth) { + $fill = ' ' unless defined $fill; + if ($left) { + $str .= $fill x ($minwidth - length $str); + } else { + $str = $fill x ($minwidth - length $str) . $str; + } + } + $res .= $str; + } + if (defined $upto) { + not $upto or + ($upto eq ']' ? $_[1] =~ s/^\]// : $_[1] =~ s/^\}//) + or die "Can't find final delimiter `$upto': I see `$_[1]'"; + } else { + die "Can't parse `$_[1]' during interpolation" if length $_[1]; + } + return $res; +} + +sub interpolate ($$) { + my ($self, $pattern) = @_; # local copy; $pattern is modified + $self->_interpolate($pattern); +} + + +=item interpolate_with_flags + + @results = $mp3->interpolate_with_flags($text, $flags); + +Processes $text according to directives in the string $flags; $flags is +split into separate flag characters; the meanings (and order of application) of +flags are + + i interpolate via $mp3->interpolate + f interpret (the result) as filename, read from file + F if file does not exist, it is not an error + B read is performed in binary mode (otherwise + in text mode, modified per + 'decode_encoding_files' configuration variable) + l split result per 'parse_split' configuration variable + n as l, using the track-number-th element (1-based) + in the result + I interpolate (again) via $mp3->interpolate + b unless present, remove leading and trailing whitespace + +With C<l>, may produce multiple results. May be accessed via +interpolation of C<%{I(flags)text}>. + +=cut + +sub interpolate_with_flags ($$$) { + my ($self, $data, $flags) = @_; + + $data = $self->interpolate($data) if $flags =~ /i/; + if ($flags =~ /f/) { + local *F; + my $e; + unless (open F, "< $data") { + return if $flags =~ /F/; + die "Can't open file `$data' for parsing: $!"; + } + if ($flags =~ /B/) { + binmode F; + } else { + my $e; + if ($e = $self->get_config('decode_encoding_files') and $e->[0]) { + eval "binmode F, ':encoding($e->[0])'"; # old binmode won't compile... + } + } + + local $/; + my $d = <F>; + CORE::close F or die "Can't close file `$data' for parsing: $!"; + $data = $d; + } + my @data = $data; + if ($flags =~ /[ln]/) { + my $p = $self->get_config('parse_split')->[0]; + @data = split $p, $data, -1; + } + if ($flags =~ /n/) { + my $track = $self->track1 or return; + @data = $data[$track - 1]; + } + for my $d (@data) { + $d = $self->interpolate($d) if $flags =~ /I/; + unless ($flags =~ /b/) { + $d =~ s/^\s+//; + $d =~ s/\s+$//; + } + } + @data; +} + +=item parse_rex($pattern, $string) + +Parse $string according to the regular expression $pattern with +C<%>-escapes C<%%, %a, %t, %l, %y, %g, %c, %n, %e, %E>. The meaning +of escapes is the same as for method L<"interpolate">(); but they are +used not for I<expansion>, but for I<matching> a part of $string +suitable to be a value for these fields. Returns false on failure, a +hash reference with parsed fields otherwise. + +Some more escapes are supported: C<%=a, %=t, %=l, %=y, %=g, %=c, %=n, %=e, +%=E, %=A, %=B, %=D, %=f, %=F, %=N, %={WHATEVER}> I<match> +substrings which are I<current> values of artist/title/etc (C<%=n> also +matches leading 0s; actual file-name matches ignore the difference +between C</> and C<\>, between one and multiple consequent dots (if +configuration variable C<parse_filename_merge_dots> is true (default)) +and are case-insensitive if configuration variable +C<parse_filename_ignore_case> is true (default); moreover, C<%n>, +C<%y>, C<%=n>, C<%=y> will not match if the string-to-match is +adjacent to a digit). + +The escapes C<%{UE<lt>numberE<gt>}> and escapes of the forms +C<%{ABCD}>, C<%{ABCDE<lt>numberE<gt>}> match any string; the +corresponding hash key in the result hash is what is inside braces; +here C<ABCD> is a 4-letter word possibly followed by 2-digit number +(as in names of ID3v2 tags), or what can be put in +C<'%{FRAM(lang,list)[description]}'>. + + $res = $mp3->parse_rex( qr<^%a - %t\.\w{1,4}$>, + $mp3->filename_nodir ) or die; + $author = $res->{author}; + +2-digit numbers, or I<number1/number2> with number1,2 up to 999 are +allowed for the track number (the leading 0 is stripped); 4-digit +years in the range 1000..2999 are allowed for year. Alternatively, if +option year_is_timestamp is TRUE (default), year may be a range of +timestamps in the format understood by ID3v2 method year() (see +L<MP3::Tag::ID3v2/"year">). + +Currently the regular expressions with capturing parens are not supported. + +=item parse_rex_prepare($pattern) + +Returns a data structure which later can be used by parse_rex_match(). +These two are equivalent: + + $mp3->parse_rex($pattern, $data); + $mp3->parse_rex_match($mp3->parse_rex_prepare($pattern), $data); + +This call constitutes the "slow part" of the parse_rex() call; it makes sense to +factor out this step if the parse_rex() with the same $pattern is called +against multiple $data. + +=item parse_rex_match($prepared, $data) + +Matches $data against a data structure returned by parse_rex_prepare(). +These two are equivalent: + + $mp3->parse_rex($pattern, $data); + $mp3->parse_rex_match($mp3->parse_rex_prepare($pattern), $data); + +=cut + +sub _rex_protect_filename { + my ($self, $filename, $what) = (shift, quotemeta shift, shift); + $filename =~ s,\\[\\/],[\\\\/],g; # \ and / are interchangeable + backslashitis + if ($self->get_config('parse_filename_merge_dots')->[0]) { + # HPFS doesn't distinguish x..y and x.y + $filename =~ s(\\\.+)(\\.+)g; + $filename =~ s($)(\\.*) if $what =~ /[ABN]/; + } + my $case = $self->get_config('parse_filename_ignore_case')->[0]; + return $filename unless $case; + return "(?i:$filename)"; +} + +sub _parse_rex_anything ($$) { + my $c = shift->get_config('parse_minmatch'); + my $min = $c->[0]; + if ($min and $min ne '1') { + my $field = shift; + $min = grep $_ eq $field, @$c; + } + return $min ? '(.*?)' : '(.*)'; +} + +sub __pure_track_rex ($) { + my $t = shift()->track; + $t =~ s/^0+//; + $t =~ s,^(.*?)(/.*),\Q$1\E(?:\Q$2\E)?,; + $t +} + +sub _parse_rex_microinterpolate { # $self->idem($code, $groups, $ecount) + my ($self, $code, $groups) = (shift, shift, shift); + return '%' if $code eq '%'; + # In these two, allow setting to '', and to 123/789 too... + push(@$groups, $code), return '((?<!\d)\d{1,3}(?:/\d{1,3})?(?!\d)|\A\Z)' if $code eq 'n'; + (push @$groups, $code), return '((?<!\d)[12]\d{3}(?:(?:--|[-:/T\0,])\d(?:|\d|\d\d\d))*(?!\d)|\A\Z)' + if $code eq 'y' and ($self->get_config('year_is_timestamp'))->[0]; + (push @$groups, $code), return '((?<!\d)[12]\d{3}(?!\d)|\A\Z)' + if $code eq 'y'; + # Filename parts ABDfFN and vLrqQSmsCpouMH not settable... + (push @$groups, $code), return $self->_parse_rex_anything($code) + if $code =~ /^[talgc]$/; + $_[0]++, return $self->_rex_protect_filename($self->interpolate("%$1"), $1) + if $code =~ /^=([ABDfFN]|{d\d+})$/; + $_[0]++, return quotemeta($self->interpolate("%$1")) + if $code =~ /^=([talgceEwhvLrqQSmsCpouMH]|{.*})$/; + $_[0]++, return '(?<!\d)0*' . $self->__pure_track_rex . '(?!\d)' + if $code eq '=n'; + $_[0]++, return '(?<!\d)' . quotemeta($self->year) . '(?!\d)' + if $code eq '=y'; + (push @$groups, $1), return $self->_parse_rex_anything() + if $code =~ /^{(U\d+|\w{4}(\d\d+|(?:\([^\)]*\))?(?:\[.*\])?)?)}$/s; + # What remains is extension + my $e = $self->get_config('extension')->[0]; + (push @$groups, $code), return "($e)" if $code eq 'E'; + (push @$groups, $code), return "(?<=(?=(?:$e)\$)\\.)(.*)" if $code eq 'e'; + # Check whether '=' was omitted, as in %f + $code =~ /^=/ or + eval {my ($a, $b); $self->_parse_rex_microinterpolate("=$code", $a, $b)} + and die "escape `%$code' can't be parsed; did you forget to put `='?"; + die "unknown escape `%$code'"; +} + +sub parse_rex_prepare { + my ($self, $pattern) = @_; + my ($codes, $exact, $p) = ([], 0, ''); + my $o = $pattern; + # (=? is correct! Group 4 is inside $frame_bra + while ($pattern =~ s<^([^%]+)|%(=?{(?:($frame_bra)|[^}]+})|=?.)><>so) { + if (defined $1) { + $p .= $1; + } else { + my $group = $2; + # description begins + $group .= ($self->_interpolate($pattern, ']') . ']') if $4; + if ($3) { + $pattern =~ s/^}// or die "Can't find end of frame name, I see `$p'"; + $group .= '}'; + } + $p .= $self->_parse_rex_microinterpolate($group, $codes, $exact); + } + } + die "Can't parse pattern, I see `$pattern'" if length $pattern; + #$pattern =~ s<%(=?{(?:[^\\{}]|\\[\\{}])*}|{U\d+}|=?.)> # (=? is correct! + # ( $self->_parse_rex_microinterpolate($1, $codes, $exact) )seg; + my @tags = map { length == 1 ? $trans{$_} : $_ } @$codes; + return [$o, $p, \@tags, $exact]; +} + +sub parse_rex_match { # pattern = [Original, Interpolated, Fields, NumExact] + my ($self, $pattern, $data) = @_; + return unless @{$pattern->[2]} or $pattern->[3]; + my @vals = ($data =~ /$pattern->[1]()/s) or return; # At least 1 group + my $cv = @vals - 1; + die "Unsupported %-regular expression `$pattern->[0]' (catching parens? Got $cv vals) (converted to `$pattern->[1]')" + unless $cv == @{$pattern->[2]}; + my ($c, %h) = 0; + for my $k ( @{$pattern->[2]} ) { + $h{$k} ||= []; + push @{ $h{$k} }, $vals[$c++]; # Support multiple occurences + } + my $j = $self->get_config('parse_join')->[0]; + for $c (keys %h) { + $h{$c} = join $j, grep length, @{ $h{$c} }; + } + $h{track} =~ s/^0+(?=\d)// if exists $h{track}; + return \%h; +} + +sub parse_rex { + my ($self, $pattern, $data) = @_; + $self->parse_rex_match($self->parse_rex_prepare($pattern), $data); +} + +=item parse($pattern, $string) + +Parse $string according to the string $pattern with C<%>-escapes C<%%, +%a, %t, %l, %y, %g, %c, %n, %e, %E>. The meaning of escapes is the +same as for L<"interpolate">. See L<"parse_rex($pattern, $string)"> +for more details. Returns false on failure, a hash reference with +parsed fields otherwise. + + $res = $mp3->parse("%a - %t.mp3", $mp3->filename_nodir) or die; + $author = $res->{author}; + +2-digit numbers are allowed for the track number; 4-digit years in the range +1000..2999 are allowed for year. + +=item parse_prepare($pattern) + +Returns a data structure which later can be used by parse_rex_match(). +This is a counterpart of parse_rex_prepare() used with non-regular-expression +patterns. These two are equivalent: + + $mp3->parse($pattern, $data); + $mp3->parse_rex_match($mp3->parse_prepare($pattern), $data); + +This call constitutes the "slow part" of the parse() call; it makes sense to +factor out this step if the parse() with the same $pattern is called +against multiple $data. + +=cut + +#my %unquote = ('\\%' => '%', '\\%\\=' => '%='); +sub __unquote ($) { (my $k = shift) =~ s/\\(\W)/$1/g; $k } + +sub parse_prepare { + my ($self, $pattern) = @_; + $pattern = "^\Q$pattern\E\$"; + # unquote %. and %=. and %={WHATEVER} and %{WHATEVER} + $pattern =~ s<(\\%(?:\\=)?(\w|\\{(?:\w|\\[^\w\\{}]|\\\\\\[\\{}])*\\}|\\\W))> + ( __unquote($1) )ge; + # $pattern =~ s/(\\%(?:\\=)?)(\w|\\(\W))/$unquote{$1}$+/g; + return $self->parse_rex_prepare($pattern); +} + +sub parse { + my ($self, $pattern, $data) = @_; + $self->parse_rex_match($self->parse_prepare($pattern), $data); +} + +=item filename() + +=item abs_filename() + +=item filename_nodir() + +=item filename_noextension() + +=item filename_nodir_noextension() + +=item abs_filename_noextension() + +=item dirname([$strip_levels]) + +=item filename_extension() + +=item filename_extension_nodot() + +=item dir_component([$level]) + + $filename = $mp3->filename(); + $abs_filename = $mp3->abs_filename(); + $filename_nodir = $mp3->filename_nodir(); + $abs_dirname = $mp3->dirname(); + $abs_dirname = $mp3->dirname(0); + $abs_parentdir = $mp3->dirname(1); + $last_dir_component = $mp3->dir_component(0); + +Return the name of the audio file: either as given to the new() method, or +absolute, or directory-less, or originally given without extension, or +directory-less without extension, or +absolute without extension, or the directory part of the fullname only, or +filename extension (with dot included, or not). + +The extension is calculated using the config() value C<extension>. + +The dirname() method takes an optional argument: the number of directory +components to strip; the C<dir_component($level)> method returns one +component of the directory (to get the last use 0 as $level; this is the +default if no $level is specified). + +The configuration option C<decode_encoding_filename> can be used to +specify the encoding of the filename; all these functions would use +filename decoded from this encoding. + +=cut + +sub from_filesystem ($$) { + my ($self, $f) = @_; + my $e = $self->get_config('decode_encoding_filename'); + return $f unless $e and $e->[0]; + require Encode; + Encode::decode($e->[0], $f); +} + +sub filename { + my $self = shift; + $self->from_filesystem($self->{ofilename}); +} + +sub abs_filename { + my $self = shift; + $self->from_filesystem($self->{abs_filename}); +} + +sub filename_noextension { + my $self = shift; + my $f = $self->filename; + my $ext_re = $self->get_config('extension')->[0]; + $f =~ s/$ext_re//; + return $f; +} + +sub filename_nodir { + require File::Basename; + return scalar File::Basename::fileparse(shift->filename, ""); +} + +sub dirname { + require File::Basename; + my ($self, $l) = (shift, shift); + my $p = $l ? $self->dirname($l - 1) : $self->abs_filename; + return File::Basename::dirname($p); +} + +sub dir_component { + require File::Basename; + my ($self, $l) = (shift, shift); + return scalar File::Basename::fileparse($self->dirname($l), ""); +} + +sub filename_extension { + my $self = shift; + my $f = $self->filename_nodir; + my $ext_re = $self->get_config('extension')->[0]; + $f =~ /($ext_re)/ or return ''; + return $1; +} + +sub filename_nodir_noextension { + my $self = shift; + my $f = $self->filename_nodir; + my $ext_re = $self->get_config('extension')->[0]; + $f =~ s/$ext_re//; + return $f; +} + +sub abs_filename_noextension { + my $self = shift; + my $f = $self->abs_filename; + my $ext_re = $self->get_config('extension')->[0]; + $f =~ s/$ext_re//; + return $f; +} + +sub filename_extension_nodot { + my $self = shift; + my $e = $self->filename_extension; + $e =~ s/^\.//; + return $e; +} + +=item mpeg_version() + +=item mpeg_layer() + +=item mpeg_layer_roman() + +=item is_stereo() + +=item is_vbr() + +=item bitrate_kbps() + +=item frequency_Hz() + +=item frequency_kHz() + +=item size_bytes() + +=item total_secs() + +=item total_secs_int() + +=item total_secs_trunc() + +=item total_millisecs_int() + +=item total_mins() + +=item leftover_mins() + +=item leftover_secs() + +=item leftover_secs_float() + +=item leftover_secs_trunc() + +=item leftover_msec() + +=item time_mm_ss() + +=item is_copyrighted() + +=item is_copyrighted_YN() + +=item frames_padded() + +=item frames_padded_YN() + +=item channel_mode_int() + +=item frames() + +=item frame_len() + +=item vbr_scale() + +These methods return the information about the contents of the MP3 +file. If this information is not cached in ID3v2 tags (not +implemented yet), using these methods requires that the module +L<MP3::Info|MP3::Info> is installed. Since these calls are +redirectoed to the module L<MP3::Info|MP3::Info>, the returned info is +subject to the same restrictions as the method get_mp3info() of this +module; in particular, the information about the frame number and +frame length is only approximate. + +vbr_scale() is from the VBR header; total_secs() is not necessarily an +integer, but total_secs_int() and total_secs_trunc() are (first is +rounded, second truncated); time_mm_ss() has format C<MM:SS>; the +C<*_YN> flavors return the value as a string Yes or No; +mpeg_layer_roman() returns the value as a roman numeral; +channel_mode() takes values in C<'stereo', 'joint stereo', 'dual +channel', 'mono'>. + +=cut + +my %mp3info = qw( + mpeg_version VERSION + mpeg_layer LAYER + is_stereo STEREO + is_vbr VBR + bitrate_kbps BITRATE + frequency_kHz FREQUENCY + size_bytes SIZE + is_copyrighted COPYRIGHT + frames_padded PADDING + channel_mode_int MODE + frames FRAMES + frame_len FRAME_LENGTH + vbr_scale VBR_SCALE + total_secs_fetch SECS +); + +# Obsoleted: +# total_mins MM +# time_mm_ss TIME +# leftover_secs SS +# leftover_msec MS + +for my $elt (keys %mp3info) { + no strict 'refs'; + my $k = $mp3info{$elt}; + *$elt = sub (;$) { + # $MP3::Info::try_harder = 1; # Bug: loops infinitely if no frames + my $self = shift; + my $info = $self->{mp3info}; + unless ($info) { + require MP3::Info; + $info = MP3::Info::get_mp3info($self->abs_filename); + die "Didn't get valid data from MP3::Info for `".($self->abs_filename)."': $@" + unless defined $info; + } + $info->{$k} + } +} + +sub frequency_Hz ($) { + 1000 * (shift->frequency_kHz); +} + +sub mpeg_layer_roman { eval { 'I' x (shift->mpeg_layer) } || '' } +sub total_millisecs_int_fetch { int (0.5 + 1000 * shift->duration_secs) } +sub frames_padded_YN { eval {shift->frames_padded() ? 'Yes' : 'No' } || '' } +sub is_copyrighted_YN { eval {shift->is_copyrighted() ? 'Yes' : 'No' } || '' } + +sub total_millisecs_int { + my $self = shift; + my $ms = $self->{ms}; + return $ms if defined $ms; + (undef, $ms) = $self->get_id3v2_frames('TLEN'); + $ms = $self->total_millisecs_int_fetch() unless defined $ms; + $self->{ms} = $ms; + return $ms; +} +sub total_secs_int { int (0.5 + 0.001 * shift->total_millisecs_int) } +sub total_secs { 0.001 * shift->total_millisecs_int } +sub total_secs_trunc { int (0.001 * (0.5 + shift->total_millisecs_int)) } +sub total_mins { int (0.001/60 * (0.5 + shift->total_millisecs_int)) } +sub leftover_mins { shift->total_mins() % 60 } +sub total_hours { int (0.001/60/60 * (0.5 + shift->total_millisecs_int)) } +sub leftover_secs { shift->total_secs_int() % 60 } +sub leftover_secs_trunc { shift->total_secs_trunc() % 60 } +sub leftover_msec { shift->total_millisecs_int % 1000 } +sub leftover_secs_float { shift->total_millisecs_int % 60000 / 1000 } +sub time_mm_ss { # Borrowed from MP3::Info + my $self = shift; + sprintf "%.2d:%.2d", $self->total_mins, $self->leftover_secs; +} + +sub duration_secs { # Tricky: in which order to query MP3::Info and ExifTool? + my $self = shift; + my $d = $self->{duration}; + return $d if defined $d; # Cached value + return $self->{duration} = $self->total_secs_fetch # Have MP3::Info or a chance to work + if $self->{mp3info} or $self->{filename} =~ /\.mp[23]$/i; + my $r = $self->_duration; # Next: try ExifTool + $r = $self->total_secs_fetch unless $r; # Try MP3::Info anyway + return $r; +} + +=item format_time + + $output = $mp3->format_time(67456.123, @format); + +formats time according to @format, which should be a list of format +descriptors. Each format descriptor is either a simple letter, or a +string in braces appropriate to be put after C<%> in an interpolated +string. A format descriptor can be followed by a literal string to be +put as a suffix, and can be preceded by a question mark, which says +that this part of format should be printed only if needed. + +Leftover minutes, seconds are formated 0-padded to width 2 if they are +preceded by more coarse units. Similarly, leftover milliseconds are +printed with leading dot, and 0-padded to width 3. + +Two examples of useful C<@format>s are + + qw(?H: ?{mL}: {SML}) + qw(?Hh ?{mL}m {SL} ?{ML}) + +Both will print hours, minutes, and milliseconds only if needed. The +second one will use 3 digit-format after a point, the first one will +not print the trailing 0s of milliseconds. The first one uses C<:> as +separator of hours and minutes, the second one will use C<h m>. + +Optionally, the first element of the array may be of the form +C<=E<gt>U>, here C<U> is one of C<h m s>. In this case, duration is +rounded to closest hours, min or second before processing. (E.g., +1.7sec would print as C<1> with C<@format>s above, but would print as +C<2> if rounded to seconds.) + +=cut + +my %Unit = qw( h 3600 m 60 s 1 ); + +sub format_time { + my ($self, $time) = (shift, shift); + $self = $self->new_fake() unless ref $self; + local $self->{ms} = $self->{ms}; # Make modifiable + local $self->{ms} = int($time * 1000 + 0.5) if defined $time; + my ($out, %have, $c) = ''; + for my $f (@_) { + $have{$+}++ if $f =~ /^\??({([^{}]+)}|.)/; + } + for my $f (@_) { + if (!$c++ and $f =~ /^=>(\w)$/) { + my $u = $Unit{$1} or die "Unexpected unit of time for rounding: `$1'"; + $time = $self->total_secs unless defined $time; + $time = $u * int($time/$u + 0.5); + $self->{ms} = 1000 * $time; + next; + } + my $ff = $f; # Modifiable + my $opt = ($ff =~ s/^\?//); + $ff =~ s/^({[^{}]+}|\w)// or die "unexpected time format: <<$f>>"; + my ($what, $format) = ($1, ''); + if ($opt) { + if ($what eq 'H') { + $time = $self->total_secs unless defined $time; + $opt = int($time / 3600) || !(grep $have{$_}, qw(m mL s S SL SML)); + } elsif ($what eq 'm' or $what eq '{mL}') { + $time = $self->total_secs unless defined $time; + $opt = int($time / 60) || !(grep $have{$_}, qw(s S SL SML)); + } elsif ($what eq '{ML}') { + $opt = ($time != int $time); + } else { + $opt = 1; + #die "Do not know how to treat optional `$what'"; + } + $what =~ /^(?:{(.*)}|(.))/ or die; + (delete $have{$+}), next unless $opt; + } + $format = '02' + if (($what eq 's' or $what eq '{SL}') and (grep $have{$_}, qw(H m mL))) + or $what eq '{mL}' and $have{H}; + $what = "%$format$what"; + $what = ".%03{ML}" + if $what eq '%{ML}' and grep $have{$_}, qw(H m mL s S SL); + if ($what eq '%{SML}' and grep $have{$_}, qw(H m mL)) { # manual padding + my $res = $self->interpolate($what); + $res = "0$res" unless $res =~ /^\d\d/; + $out .= "$res$ff"; + } else { + $out .= $self->interpolate($what) . $ff; + } + } + $out; +} + +my @channel_modes = ('stereo', 'joint stereo', 'dual channel', 'mono'); +sub channel_mode { $channel_modes[shift->channel_mode_int] } + +=item can_write() + +checks permission to write per the configuration variable C<is_writable>. + +=item can_write_or_die($mess) + +as can_write(), but die()s on non-writable files with meaningful error message +($mess is prepended to the message). + +=item die_cant_write($mess) + +die() with the same message as can_write_or_die(). + +=item writable_by_extension() + +Checks that extension is (case-insensitively) in the list given by +configuration variable C<writable_extensions>. + +=cut + +sub can_write ($) { + my $self = shift; + my @wr = @{ $self->get_config('is_writable') }; # Make copy + return $wr[0] if @wr == 1 and not $wr[0] =~ /\D/; + my $meth = shift @wr; + $self->$meth(@wr); +} + +sub writable_by_extension ($) { + my $self = shift; + my $wr = $self->get_config('writable_extensions'); # Make copy + $self->extension_is(@$wr); +} + +sub die_cant_write ($$) { + my($self, $what) = (shift, shift); + die $what, $self->interpolate("File %F is not writable per `is_writable' confuration variable, current value is `"), + join(', ', @{$self->get_config('is_writable')}), "'"; +} + +sub can_write_or_die ($$) { + my($self, $what) = (shift, shift); + my $wr = $self->can_write; + return $wr if $wr; + $self->die_cant_write($what); +} + +=item update_tags( [ $data, [ $force2 ]] ) + + $mp3 = MP3::Tag->new($filename); + $mp3->update_tags(); # Fetches the info, and updates tags + + $mp3->update_tags({}); # Updates tags if needed/changed + + $mp3->update_tags({title => 'This is not a song'}); # Updates tags + +This method updates ID3v1 and ID3v2 tags (the latter only if in-memory copy +contains any data, or $data does not fit ID3v1 restrictions, or $force2 +argument is given) +with the the information about title, artist, album, year, comment, track, +genre from the hash reference $data. The format of $data is the same as +one returned from autoinfo() (with or without the optional argument 'from'). +The fields which are marked as coming from ID3v1 or ID3v2 tags are not updated +when written to the same tag. + +If $data is not defined or missing, C<autoinfo('from')> is called to obtain +the data. Returns the object reference itself to simplify chaining of method +calls. + +This is probably the simplest way to set data in the tags: populate +$data and call this method - no further tinkering with subtags is +needed. + +=cut + +sub update_tags { + my ($mp3, $data, $force2, $wr2) = (shift, shift, shift); + + $mp3->get_tags; + $data = $mp3->autoinfo('from') unless defined $data; + +# $mp3->new_tag("ID3v1") unless $wr1 = exists $mp3->{ID3v1}; + unless (exists $mp3->{ID3v1}) { + $mp3->can_write_or_die('update_tags() doing ID3v1: '); + $wr2 = 1; + $mp3->new_tag("ID3v1"); + } + my $elt; + for $elt (qw/title artist album year comment track genre/) { + my $d = $data->{$elt}; + next unless defined $d; + $d = [$d, ''] unless ref $d; + $mp3->{ID3v1}->$elt( $d->[0] ) if $d->[1] ne 'ID3v1'; + } # Skip what is already there... + $mp3->{ID3v1}->write_tag; + + my $do_length + = (defined $mp3->{ms}) ? ($mp3->get_config('update_length'))->[0] : 0; + + return $mp3 + if not $force2 and $mp3->{ID3v1}->fits_tag($data) + and not exists $mp3->{ID3v2} and $do_length < 2; + +# $mp3->new_tag("ID3v2") unless exists $mp3->{ID3v2}; + unless (exists $mp3->{ID3v2}) { + if (defined $wr2) { + $mp3->die_cant_write('update_tags() doing ID3v2: ') unless $wr2; + } else { + $mp3->can_write_or_die('update_tags() doing ID3v2: '); + } + $mp3->new_tag("ID3v2"); + } + for $elt (qw/title artist album year comment track genre/) { + my $d = $data->{$elt}; + next unless defined $d; + $d = [$d, ''] unless ref $d; + $mp3->{ID3v2}->$elt( $d->[0] ) if $d->[1] ne 'ID3v2'; + } # Skip what is already there... + # $mp3->{ID3v2}->comment($data->{comment}->[0]); + + $mp3->set_id3v2_frame('TLEN', $mp3->{ms}) + if $do_length and not $mp3->have_id3v2_frame('TLEN'); + $mp3->{ID3v2}->write_tag; + return $mp3; +} + +sub _massage_genres ($;$) { # Thanks to neil verplank for the prototype + require MP3::Tag::ID3v1; + my($data, $how) = (shift, shift); + my $firstnum = (($how || 0) eq 'num'); + my $prefer_num = (($how || 0) eq 'prefer_num'); + my (%seen, @genres); # find all genres in incoming data + $data = $data->[0] if ref $data; + # clean and split line on both null and parentheses + $data =~ s/\s+/ /g; + $data =~ s/\s*\0[\0\s]*/\0/g; + $data =~ s/^[\s\0]+//; + $data =~ s/[\s\0]+$//; + my @data = split m<\0|\s+/\s+>, $data; + @data = split /\( ( \d+ | rx | cr ) \)/xi, $data[0] if @data == 1; + + # review array, produce a clean, ordered list of unique genres for output + foreach my $genre (@data) { + next if $genre eq ""; # (12)(13) ==> in front, and between + + # convert text to number to eliminate collisions, and produce consistent output + if ($genre =~ /\D/) {{ # Not a pure number + # return id number + my $genre_num = MP3::Tag::ID3v1::genres($genre); + # 255 is "non-standard text" in ID3v1; pass the rest through + last if $genre_num eq '255' or $genre_num eq ''; + return $genre_num if $firstnum; + $genre = $genre_num, last if $prefer_num; + $genre_num = MP3::Tag::ID3v1::genres($genre_num); + last unless defined $genre_num; + $genre = $genre_num; + }} # Now converted to a number - if possible + unless ($prefer_num or $genre =~ /\D/) {{ # Here $genre is a number + my $genre_str = MP3::Tag::ID3v1::genres($genre) or last; + return $genre if $firstnum; + $genre = $genre_str; + }} + # 2.4 defines these conversions + $genre = "Remix" if lc $genre eq "rx"; + $genre = "Cover" if lc $genre eq "cr"; + $genre = "($genre)" if length $genre and not $genre =~ /\D/; # Only digits + push @genres, $genre unless $seen{$genre}++; + } + return if $firstnum; + @genres; +} + +=item extension_is + + $mp3->extension_is(@EXT_LIST) + +returns TRUE if the extension of the filename coincides (case-insensitive) +with one of the elements of the list. + +=cut + +sub extension_is ($@) { + my ($self) = (shift); + my $ext = lc($self->filename_extension_nodot()); + return 1 if grep $ext eq lc, @_; + return; +} + +sub DESTROY { + my $self=shift; + if (exists $self->{filename} and defined $self->{filename}) { + $self->{filename}->close; + } +} + +sub parse_cfg_line ($$$) { + my ($self, $line, $data) = (shift,shift,shift); + return if $line =~ /^\s*(#|$)/; + die "Unrecognized configuration file line: <<<$line>>>" + unless $line =~ /^\s*(\w+)\s*=\s*(.*?)\s*$/; + push @{$data->{$1}}, $2; +} + +=item C<parse_cfg( [$filename] )> + +Reads configuration information from the specified file (defaults to +the value of configuration variable C<local_cfg_file>, which is +C<~>-substituted). Empty lines and lines starting with C<#> are ignored. +The remaining lines should have format C<varname=value>; leading +and trailing whitespace is stripped; there may be several lines with the same +C<varname>; this sets list-valued variables. + +=back + +=cut + +sub parse_cfg ($;$) { + my ($self, $file) = (shift,shift); + $file = ($self->get_config('local_cfg_file'))->[0] unless defined $file; + return unless defined $file; + $file =~ s,^~(?=[/\\]),$ENV{HOME}, if $ENV{HOME}; + return unless -e $file; + open F, "< $file" or die "Can't open `$file' for read: $!"; + my $data = {}; + while (defined (my $l = <F>)) { + $self->parse_cfg_line($l, $data); + } + CORE::close F or die "Can't close `$file' for read: $!"; + for my $k (keys %$data) { + $self->config($k, @{$data->{$k}}); + } +} + +my @parents = qw(User Site Vendor); + +@MP3::Tag::User::ISA = qw( MP3::Tag::Site MP3::Tag::Vendor + MP3::Tag::Implemenation ); # Make overridable +@MP3::Tag::Site::ISA = qw( MP3::Tag::Vendor MP3::Tag::Implemenation ); +@MP3::Tag::Vendor::ISA = qw( MP3::Tag::Implemenation ); + +sub load_parents { + my $par; + while ($par = shift @parents) { + return 1 if eval "require MP3::Tag::$par; 1" + } + return; +} +load_parents() unless $ENV{MP3TAG_SKIP_LOCAL}; +MP3::Tag->parse_cfg() unless $ENV{MP3TAG_SKIP_LOCAL}; + +1; + +=pod + +=head1 ENVIRONMENT + +Some defaults for the operation of this module (and/or scripts distributed +with this module) are set from +environment. Assumed encodings (0 or encoding name): for read access: + + MP3TAG_DECODE_V1_DEFAULT MP3TAG_DECODE_V2_DEFAULT + MP3TAG_DECODE_FILENAME_DEFAULT MP3TAG_DECODE_FILES_DEFAULT + MP3TAG_DECODE_INF_DEFAULT MP3TAG_DECODE_CDDB_FILE_DEFAULT + MP3TAG_DECODE_CUE_DEFAULT + +for write access: + + MP3TAG_ENCODE_V1_DEFAULT MP3TAG_ENCODE_FILES_DEFAULT + +(if not set, default to corresponding C<DECODE> options). + +Defaults for the above: + + MP3TAG_DECODE_DEFAULT MP3TAG_ENCODE_DEFAULT + +(if the second one is not set, the value of the first one is used). +Value 0 for more specific variable will cancel the effect of the less +specific variables. + +These variables set default configuration settings for C<MP3::Tag>; +the values are read during the load time of the module. After load, +one can use config()/get_config() methods to change/access these +settings. See C<encode_encoding_*> and C<encode_decoding_*> in +documentation of L<config|config> method. (Note that C<FILES> variant +govern file read/written in non-binary mode by L<MP3/ParseData> module, +as well as reading of control files of some scripts using this module, such as +L<typeset_audio_dir>.) + +=over + +=item B<EXAMPLE> + +Assume that locally present CDDB files and F<.inf> files +are in encoding C<cp1251> (this is not supported by "standard", but since +the standard supports only a handful of languages, this is widely used anyway), +and that one wants C<ID3v1> fields to be in the same encoding, but C<ID3v2> +have an honest (Unicode, if needed) encoding. Then set + + MP3TAG_DECODE_INF_DEFAULT=cp1251 + MP3TAG_DECODE_CDDB_FILE_DEFAULT=cp1251 + MP3TAG_DECODE_V1_DEFAULT=cp1251 + +Since C<MP3TAG_DECODE_V1_DEFAULT> implies C<MP3TAG_ENCODE_V1_DEFAULT>, +you will get the desired effect both for read and write of MP3 tags. + +=back + +Additionally, the following (unsupported) variables are currently +recognized by ID3v2 code: + + MP3TAG_DECODE_UNICODE MP3TAG_DECODE_UTF8 + +MP3TAG_DECODE_UNICODE (default 1) enables decoding; the target of +decoding is determined by MP3TAG_DECODE_UTF8: if 0, decoded values are +byte-encoded UTF-8 (every Perl character contains a byte of UTF-8 +encoded string); otherwise (default) it is a native Perl Unicode +string. + +If C<MP3TAG_SKIP_LOCAL> is true, local customization files are not loaded. + +=head1 CUSTOMIZATION + +Many aspects of operation of this module are subject to certain subtle +choices. A lot of effort went into making these choices customizable, +by setting global or per-object configuration variables. + +A certain degree of customization of global configuration variables is +available via the environment variables. Moreover, at startup the local +customization file F<~/.mp3tagprc> is read, and defaults are set accordingly. + +In addition, to make customization as flexible as possible, I<ALL> aspects +of operation of C<MP3::Tag> are subject to local override. Three customization +modules + + MP3::Tag::User MP3::Tag::Site MP3::Tag::Vendor + +are attempted to be loaded if present. Only the first module (of +those present) is loaded directly; if sequential load is desirable, +the first thing a customization module should do is to call + + MP3::Tag->load_parents() + +method. + +The customization modules have an opportunity to change global +configuration variables on load. To allow more flexibility, they may +override any method defined in C<MP3::Tag>; as usual, the overriden +method may be called using C<SUPER> modifier (see L<perlobj/"Method +invocation">). + +E.g., it is recommended to make a local customization file with + + eval 'require Normalize::Text::Music_Fields'; + for my $elt ( qw( title track artist album comment year genre + title_track artist_collection person ) ) { + no strict 'refs'; + MP3::Tag->config("translate_$elt", \&{"Normalize::Text::Music_Fields::normalize_$elt"}) + if defined &{"Normalize::Text::Music_Fields::normalize_$elt"}; + } + MP3::Tag->config("short_person", \&Normalize::Text::Music_Fields::short_person) + if defined &Normalize::Text::Music_Fields::short_person; + +and install the (supplied, in the F<examples/modules>) module +L<Normalize::Text::Music_Fields> which enables normalization of person +names (to a long or a short form), and of music piece names to +canonical forms. + +To simplify debugging of local customization, it may be switched off +completely by setting MP3TAG_SKIP_LOCAL to TRUE (in environment). + +For example, putting + + id3v23_unsync = 0 + +into F<~/.mp3tagprc> will produce broken ID3v2 tags (but those required +by ITunes). + +=head1 EXAMPLE SCRIPTS + +Some example scripts come with this module (either installed, or in directory +F<examples> in the distribution); they either use this module, or +provide data understood by this module: + +=over + +=item mp3info2 + +perform command line manipulation of audio tags (and more!); + +=item audio_rename + +rename audio files according to associated tags (and more!); + +=item typeset_mp3_dir + +write LaTeX files suitable for CD covers and normal-size sheet +descriptions of hierarchy of audio files; + +=item mp3_total_time + +Calculate total duration of audio files; + +=item eat_wav_mp3_header + +remove WAV headers from MP3 files in WAV containers. + +=item fulltoc_2fake_cddb + +converts a CD's "full TOC" to a "fake" CDDB file (header only). Create +this file with something like + + readcd -fulltoc dev=0,1,0 -f=audio_cd >& nul + +run similar to + + fulltoc_2fake_cddb < audio_cd.toc | cddb2cddb > cddb.out + +=item dir_mp3_2fake_cddb + +tries to convert a directory of MP3 files to a "fake" CDDB file (header only); +assumes that files are a rip from a CD, and that alphabetical sort gives +the track order (works only heuristically, since quantization of duration +of MP3 files and of CD tracks is so different). + +Run similar to + + dir_mp3_2fake_cddb | cddb2cddb > cddb.out + + +=item inf_2fake_cddb + +tries to convert a directory of F<.inf> files to a "fake" CDDB file (header +only). (Still heuristic, since it can't guess the length of the leader.) + +Run similar to + + inf_2fake_cddb | cddb2cddb > cddb.out + +=item cddb2cddb + +Reads a (header of) CDDB file from STDIN, outputs (on STDOUT) the current +version of the database record. Can be used to update a file, and/or to +convert a fake CDDB file to a real one. + +=back + +(Last four do not use these modules!) + +Some more examples: + + # Convert from one (non-standard-conforming!) encoding to another + perl -MMP3::Tag -MEncode -wle ' + my @fields = qw(artist album title comment); + for my $f (@ARGV) { + print $f; + my $t = MP3::Tag->new($f) or die; + $t->update_tags( + { map { $_ => encode "cp1251", decode "koi8-r", $t->$_() }, @fields } + ); + }' list_of_audio_files + +=head1 Problems with ID3 format + +The largest problem with ID3 format is that the first versions of these +format were absolutely broken (underspecified). It I<looks> like the newer +versions of this format resolved most of these problems; however, in reality +they did not (due to unspecified backward compatibility, and +grandfathering considerations). + +What are the problems with C<ID3v1>? First, one of the fields was C<artist>, +which does not make any sense. In particular, different people/publishers +would put there performer(s), composer, author of text/lyrics, or a combination +of these. The second problem is that the only allowed encoding was +C<iso-8859-1>; since most of languages of the world can't be expressed +in this encoding, this restriction was completely ignored, thus the +encoding is essentially "unknown". + +Newer versions of C<ID3> allow specification of encodings; however, +since there is no way to specify that the encoding is "unknown", when a +tag is automatically upgraded from C<ID3v1>, it is most probably assumed to be +in the "standard" C<iso-8859-1> encoding. Thus impossibility to +distinguish "unknown, assumed C<iso-8859-1>" from "known to be C<iso-8859-1>" +in C<ID3v2>, essentially, makes any encoding specified in the tag "unknown" +(or, at least, "untrusted"). (Since the upgrade [or a chain of upgrades] +from the C<ID3v1> tag to the C<ID3v2> tag can result in any encoding of +the "supposedly C<iso-8859-1>" tag, one cannot trust the content of +C<ID3v2> tag even if it stored as Unicode strings.) + +This is why this module provides what some may consider only lukewarm support +for encoding field in ID3v2 tags: if done fully automatic, it can allow +instant propagation of wrong information; and this propagation is in a form +which is quite hard to undo (but still possible to do with suitable settings +to this module; see L<mp3info2/"Examples on dealing with broken encodings">). + +Likewise, the same happens with the C<artist> field in C<ID3v1>. Since there +is no way to specify just "artist, type unknown" in C<ID3v2> tags, when +C<ID3v1> tag is automatically upgraded to C<ID3v2>, the content would most +probably be put in the "main performer", C<TPE1>, tag. As a result, the +content of C<TPE1> tag is also "untrusted" - it may contain, e.g., the composer. + +In my opinion, a different field should be used for "known to be +principal performer"; for example, the method performer() (and the +script F<mp3info2> shipped with this module) uses C<%{TXXX[TPE1]}> in +preference to C<%{TPE1}>. + +For example, interpolate C<%{TXXX[TPE1]|TPE1}> or C<%{TXXX[TPE1]|a}> - +this will use the frame C<TXXX> with identifier C<TPE1> if present, if not, +it will use the frame C<TPE1> (the first example), or will try to get I<artist> +by other means (including C<TPE1> frame) (the second example). + +=head1 FILES + +There are many files with special meaning to this module and its dependent +modules. + +=over 4 + +=item F<*.inf> + +Files with extension F<.inf> and the same basename as the audio file are +read by module C<MP3::Tag::Inf>, and the extracted data is merged into the +information flow according to configuration variable C<autoinfo>. + +It is assumed that these files are compatible in format to the files written +by the program F<cdda2wav>. + +=item F<audio.cddb> F<cddb.out> F<cddb.in> + +in the same directory as the audio file are read by module +C<MP3::Tag::CDDB_File>, and the extracted data is merged into the +information flow according to configuration variable C<autoinfo>. + +(In fact, the list may be customized by configuration variable C<cddb_files>.) + +=item F<audio_cd.toc> + +in the same directory as the audio file may be read by the method +id3v2_frames_autofill() (should be called explicitly) to fill the C<TXXX[MCDI-fulltoc]> +frame. Depends on contents of configuration variable C<id3v2_frames_autofill>. + +=item F<~/.mp3tagprc> + +By default, this file is read on startup (may be customized by overriding +the method parse_cfg()). By default, the name of the file is in the +configuration variable C<local_cfg_file>. + +=back + +=head1 SEE ALSO + +L<MP3::Tag::ID3v1>, L<MP3::Tag::ID3v2>, L<MP3::Tag::File>, +L<MP3::Tag::ParseData>, L<MP3::Tag::Inf>, L<MP3::Tag::CDDB_File>, +L<MP3::Tag::Cue>, L<mp3info2>, +L<typeset_audio_dir>. + +=head1 COPYRIGHT + +Copyright (c) 2000-2008 Thomas Geffert, Ilya Zakharevich. All rights reserved. + +This program is free software; you can redistribute it and/or +modify it under the terms of the Artistic License, distributed +with Perl. + +=cut + diff --git a/fhem/FHEM/lib/MP3/Tag/CDDB_File.pm b/fhem/FHEM/lib/MP3/Tag/CDDB_File.pm new file mode 100644 index 000000000..f044b73d7 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/CDDB_File.pm @@ -0,0 +1,345 @@ +package MP3::Tag::CDDB_File; + +use strict; +use File::Basename; +use File::Spec; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::CDDB_File - Module for parsing CDDB files. + +=head1 SYNOPSIS + + my $db = MP3::Tag::CDDB_File->new($filename, $track); # Name of audio file + my $db = MP3::Tag::CDDB_File->new_from($record, $track); # Contents of CDDB + + ($title, $artist, $album, $year, $comment, $track) = $db->parse(); + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::CDDB_File is designed to be called from the MP3::Tag module. + +It parses the content of CDDB file. + +The file is found in the same directory as audio file; the list of possible +file names is taken from the field C<cddb_files> if set by MP3::Tag config() +method. + +=over 4 + +=cut + + +# Constructor + +sub new_from { + my ($class, $data, $track) = @_; + bless {data => [split /\n/, $data], track => $track}, $class; +} + +sub new_setdir { + my $class = shift; + my $filename = shift; + $filename = $filename->filename if ref $filename; + $filename = dirname($filename); + return bless {dir => $filename}, $class; # bless to enable get_config() +} + +sub new_fromdir { + my $class = shift; + my $h = shift; + my $dir = $h->{dir}; + my ($found, $e); + my $l = $h->get_config('cddb_files'); + for my $file (@$l) { + my $f = File::Spec->catdir($dir, $file); + $found = $f, last if -r $f; + } + return unless $found; + local *F; + open F, "< $found" or die "Can't open `$found': $!"; + if ($e = $h->get_config('decode_encoding_cddb_file') and $e->[0]) { + eval "binmode F, ':encoding($e->[0])'"; # old binmode won't compile... + } + my @data = <F>; + close F or die "Error closing `$found': $!"; + bless {filename => $found, data => \@data, track => shift, + parent => $h->{parent}}, $class; +} + +sub new { + my $class = shift; + my $h = $class->new_setdir(@_); + $class->new_fromdir($h); +} + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + my $h = $class->new_setdir($filename); + $h->{parent} = $parent; + $class->new_fromdir($h); +} + +# Destructor + +sub DESTROY {} + +=item parse() + + ($title, $artist, $album, $year, $comment, $track) = + $db->parse($what); + +parse_filename() extracts information about artist, title, track number, +album and year from the CDDB record. $what is optional; it maybe title, +track, artist, album, year, genre or comment. If $what is defined parse() will return +only this element. + +Additionally, $what can take values C<artist_collection> (returns the value of +artist in the disk-info field DTITLE, but only if author is specified in the +track-info field TTITLE), C<title_track> (returns the title specifically from +track-info field - the C<track> may fall back to the info from disk-info +field), C<comment_collection> (processed EXTD comment), C<comment_track> +(processed EXTT comment). + +The returned year and genre is taken from DYEAR, DGENRE, EXTT, EXTD fields; +recognized prefixes in the two last fields are YEAR, ID3Y, ID3G. +The declarations of this form are stripped from the returned comment. + +An alternative +syntax "Recorded"/"Recorded on"/"Recorded in"/ is also supported; the format +of the date recognized by ID3v2::year(), or just a date field without a prefix. + +=cut + +sub return_parsed { + my ($self,$what) = @_; + if (defined $what) { + return $self->{parsed}{a_in_title} if $what =~/^artist_collection/i; + return $self->{parsed}{t_in_track} if $what =~/^title_track/i; + return $self->{parsed}{extt} if $what =~/^comment_track/i; + return $self->{parsed}{extd} if $what =~/^comment_collection/i; + return $self->{parsed}{DISCID} if $what =~/^cddb_id/i; + return $self->{parsed}{album} if $what =~/^al/i; + return $self->{parsed}{artist} if $what =~/^a/i; + return $self->{parsed}{track} if $what =~/^tr/i; + return $self->{parsed}{year} if $what =~/^y/i; + return $self->{parsed}{comment}if $what =~/^c/i; + return $self->{parsed}{genre} if $what =~/^g/i; + return $self->{parsed}{title}; + } + + return $self->{parsed} unless wantarray; + return map $self->{parsed}{$_} , qw(title artist album year comment track); +} + +my %r = ( 'n' => "\n", 't' => "\t", '\\' => "\\" ); + +sub parse_lines { + my ($self) = @_; + return if $self->{fields}; + for my $l (@{$self->{data}}) { + next unless $l =~ /^\s*(\w+)\s*=(\s*(.*))/; + my $app = $2; + $self->{fields}{$1} = "", $app = $3 unless exists $self->{fields}{$1}; + $self->{fields}{$1} .= $app; + $self->{last} = $1 if $1 =~ /\d+$/; + } + s/\\([nt\\])/$r{$1}/g for values %{$self->{fields}}; +} + +sub parse { + my ($self,$what) = @_; + return $self->return_parsed($what) if exists $self->{parsed}; + $self->parse_lines; + my %parsed; + my ($t1, $c1, $t2, $c2) = map $self->{fields}{$_}, qw(DTITLE EXTD); + my $track = $self->track; + if ($track) { + my $t = $track - 1; + ($t2, $c2) = map $self->{fields}{$_}, "TTITLE$t", "EXTT$t"; + } + my ($a, $t, $aa, $tt, $a_in_title, $t_in_track); + ($a, $t) = split /\s+\/\s+/, $t1, 2 if defined $t1; + ($a, $t) = ($t, $a) unless defined $t; + ($aa, $tt) = split /\s+\/\s+/, $t2, 2 if defined $t2; + ($aa, $tt) = ($tt, $aa) unless defined $tt; + undef $a if defined $a and $a =~ + /^\s*(<<\s*)?(Various Artists|compilation disc)\s*(>>\s*)?$/i; + undef $aa if defined $aa and $aa =~ + /^\s*(<<\s*)?(Various Artists|compilation disc)\s*(>>\s*)?$/i; + $a_in_title = $a if defined $a and length $a and defined $aa and length $aa; + $aa = $a unless defined $aa and length $aa; + $t_in_track = $tt; + $tt = $t unless defined $tt and length $tt; + + my ($y, $cat) = ($self->{fields}{DYEAR}, $self->{fields}{DGENRE}); + for my $f ($c2, $c1) { + if (defined $f and length $f) { # Process old style declarations + while ($f =~ s/^\s*((YEAR|ID3Y)|ID3G)\b:?\s*(\d+)\b\s*(([;.,]|\s-\s)\s*)?//i + || $f =~ s/(?:\s*(?:[;.,]|\s-\s))?\s*\b((YEAR|ID3Y)|ID3G)\b:?\s*(\d+)\s*([;.,]\s*)?$//i) { + $y = $3 if $2 and not $y; + $cat = $3 if not $2 and not $cat; + } + if ($f =~ s{ + ((^|[;,.]|\s+-\s) # 1,2 + \s* + (Recorded (\s+[io]n)? \s* (:\s*)? )? # 3, 4, 5 + (\d{4}([-,][-\d\/,]+)?) # 6, 7 + \b \s* (?: [.;] \s* )? + ((?:[;.,]|\s-\s|$)\s*)) # 8 + } + { + ((($self->{parent}->get_config('comment_remove_date'))->[0] + and not ($2 and $8)) + ? '' : $1) . ($2 && $8 ? $8 : '') + }xeim and not ($2 and $8)) { + # Overwrite the disk year for longer forms + $y = $6 if $3 or $7 or not $y or $c2 and $f eq $c2; + } + $f =~ s/^\s+//; + $f =~ s/\s+$//; + undef $f unless length $f; + } + } + my ($cc1, $cc2) = ($c1, $c2); + if (defined $c2 and length $c2) { # Merge unless one is truncation of another + if ( defined $c1 and length $c1 + and $c1 ne substr $c2, 0, length $c1 + and $c1 ne substr $c2, -length $c1 ) { + $c2 =~ s/\s*[.,:;]$//; + my $sep = (("$c1$c2" =~ /\n/) ? "\n" : '; '); + $c1 = "$c2$sep$c1"; + } else { + $c1 = $c2; + } + } + if (defined $cat and $cat =~ /^\d+$/) { + require MP3::Tag::ID3v1; + $cat = $MP3::Tag::ID3v1::winamp_genres[$cat] if $cat < scalar @MP3::Tag::ID3v1::winamp_genres; + } + + @parsed{ qw( title artist album year comment track genre + a_in_title t_in_track extt extd) } = + ($tt, $aa, $t, $y, $c1, $track, $cat, $a_in_title, $t_in_track, $cc2, $cc1); + $parsed{DISCID} = $self->{fields}{DISCID}; + $self->{parsed} = \%parsed; + $self->return_parsed($what); +} + + +=pod + +=item title() + + $title = $db->title(); + +Returns the title, obtained from the C<'Tracktitle'> entry of the file. + +=cut + +*song = \&title; + +sub title { + return shift->parse("title"); +} + +=pod + +=item artist() + + $artist = $db->artist(); + +Returns the artist name, obtained from the C<'Performer'> or +C<'Albumperformer'> entries (the first which is present) of the file. + +=cut + +sub artist { + return shift->parse("artist"); +} + +=pod + +=item track() + + $track = $db->track(); + +Returns the track number, stored during object creation, or queried from +the parent. + + +=cut + +sub track { + my $self = shift; + return $self->{track} if defined $self->{track}; + return if $self->{recursive} or not $self->parent_ok; + local $self->{recursive} = 1; + return $self->{parent}->track1; +} + +=item year() + + $year = $db->year(); + +Returns the year, obtained from the C<'Year'> entry of the file. (Often +not present.) + +=cut + +sub year { + return shift->parse("year"); +} + +=pod + +=item album() + + $album = $db->album(); + +Returns the album name, obtained from the C<'Albumtitle'> entry of the file. + +=cut + +sub album { + return shift->parse("album"); +} + +=item comment() + + $comment = $db->comment(); + +Returns the C<'Trackcomment'> entry of the file. (Often not present.) + +=cut + +sub comment { + return shift->parse("comment"); +} + +=item genre() + + $genre = $db->genre($filename); + +=cut + +sub genre { + return shift->parse("genre"); +} + +for my $elt ( qw( cddb_id ) ) { + no strict 'refs'; + *$elt = sub (;$) { + return shift->parse($elt); + } +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/Cue.pm b/fhem/FHEM/lib/MP3/Tag/Cue.pm new file mode 100644 index 000000000..206444439 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/Cue.pm @@ -0,0 +1,310 @@ +package MP3::Tag::Cue; + +use strict; +use File::Basename; +#use File::Spec; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::Cue - Module for parsing F<.cue> files. + +=head1 SYNOPSIS + + my $db = MP3::Tag::Cue->new($filename, $track); # Name of audio file + my $db = MP3::Tag::Cue->new_from($record, $track); # Contents of .cue file + + ($title, $artist, $album, $year, $comment, $track) = $db->parse(); + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::Cue is designed to be called from the MP3::Tag module. + +It parses the content of a F<.cue> file. + +The F<.cue> file is looked for in the same directory as audio file; one of the +following conditions must be satisfied: + +=over 4 + +=item * + +The "audio" file is specified is actually a F<.cue> file; + +=item * + +There is exactly one F<.cue> file in the directory of audio file; + +=item * + +There is exactly one F<.cue> file in the directory of audio file +with basename which is a beginning of the name of audio file. + +=item * + +There is exactly one F<.cue> file in the directory of audio file +with basename which matches (case-insensitive) a beginning of the +name of audio file. + +=back + +If no F<.cue> file is found in the directory of audio file, the same process +is repeated once one directory uplevel, with the name of the file's directory +used instead of the file name. E.g., with the files like this + + Foo/bar.cue + Foo/bar/04.wav + +audio file F<Foo/bar/04.wav> will be associated with F<Foo/bar.cue>. + +=cut + + +# Constructor + +sub new_from { + my ($class, $data, $track) = @_; + bless {data => [split /\n/, $data], track => $track}, $class; +} + +sub matches($$$) { + my ($f1, $f, $case) = (shift, shift, shift); + substr($f1, -4, 4) = ''; + return $f1 eq substr $f, 0, length $f1 if $case; + return lc $f1 eq lc substr $f, 0, length $f1; +} + +sub find_cue ($$) { + my ($f, $d, %seen) = (shift, shift); + require File::Glob; # "usual" glob() fails on spaces... + my @cue = (File::Glob::bsd_glob("$d/*.cue"), File::Glob::bsd_glob('$d/*.CUE')); + @seen{@cue} = (1) x @cue; # remove duplicates: + @cue = keys %seen; + my $c = @cue; + @cue = grep matches($_, $f, 0), @cue if @cue > 1; + @cue = grep matches($_, $f, 1), @cue if @cue > 1; + ($c, @cue) +} + +sub new_with_parent { + my ($class, $f, $p, $e, %seen, @cue) = (shift, shift, shift); + $f = $f->filename if ref $f; + $f = MP3::Tag->rel2abs($f); + if ($f =~ /\.cue$/i and -f $f) { + @cue = $f; + } else { + my $d = dirname($f); + (my $c, @cue) = find_cue($f, $d); + unless ($c) { + my $d1 = dirname($d); + (my $c, @cue) = find_cue($d, $d1); + } + } + return unless @cue == 1; + local *F; + open F, "< $cue[0]" or die "Can't open `$cue[0]': $!"; + if ($e = ($p or 'MP3::Tag')->get_config1('decode_encoding_cue_file')) { + eval "binmode F, ':encoding($e->[0])'"; # old binmode won't compile... + } + my @data = <F>; + close F or die "Error closing `$cue[0]': $!"; + bless {filename => $cue[0], data => \@data, track => shift, + parent => $p}, $class; +} + +sub new { + my ($class, $f) = (shift, shift); + $class->new_with_parent($f, undef, @_); +} + +# Destructor + +sub DESTROY {} + +=over 4 + +=item parse() + + ($title, $artist, $album, $year, $comment, $track) = + $db->parse($what); + +parse_filename() extracts information about artist, title, track number, +album and year from the F<.cue> file. $what is optional; it maybe title, +track, artist, album, year, genre or comment. If $what is defined parse() will return +only this element. + +Additionally, $what can take values C<artist_collection> (returns the value of +artist in the whole-disk-info field C<PERFORMER>, C<songwriter>. + +=cut + +sub return_parsed { + my ($self,$what) = @_; + if (defined $what) { + return $self->{parsed}{collection_performer} if $what =~/^artist_collection/i; + return $self->{parsed}{album} if $what =~/^al/i; + return $self->{parsed}{performer} if $what =~/^a/i; + return $self->{parsed}{songwriter} if $what =~/^songwriter/i; + return $self->{parsed}{track} if $what =~/^tr/i; + return $self->{parsed}{date} if $what =~/^y/i; + return $self->{parsed}{comment}if $what =~/^c/i; + return $self->{parsed}{genre} if $what =~/^g/i; + return $self->{parsed}{title}; + } + + return $self->{parsed} unless wantarray; + return map $self->{parsed}{$_} , qw(title artist album year comment track); +} + +my %r = ( 'n' => "\n", 't' => "\t", '\\' => "\\" ); + +sub parse_lines { + my ($self) = @_; +# return if $self->{fields}; + my $track_seen = ''; + my $track = $self->track; + $track = -1e100 unless $track or length $track; + for my $l (@{$self->{data}}) { + # http://digitalx.org/cuesheetsyntax.php + # http://wiki.hydrogenaudio.org/index.php?title=Cuesheet + # What about http://cue2toc.sourceforge.net/ ? Can it deal with .toc of cdrecord? + # http://www.willwap.co.uk/Programs/vbrfix.php - may inspect gap info??? + next unless $l =~ /^\s*(REM\s+)? + (GENRE|DATE|DISCID|COMMENT|PERFORMER|TITLE + |ISRC|POSTGAP|PREGAP|SONGWRITER + |FILE|INDEX|TRACK|CATALOG|CDTEXTFILE|FLAGS)\s+(.*)/x; + my $field = lc $2; + my $val = $3; + $val =~ s/^\"(.*)\"/$1/; # Ignore trailing fields after TRACK, FILE + $track_seen = $1 if $field eq 'track' and $val =~ /^0?(\d+)/; + next if length $track_seen and $track_seen != $track; + + $self->{fields}{$field} = $val; # unless exists $self->{fields}{$field}; + next if length $track_seen; + $self->{fields}{album} = $val if $field eq 'title'; + $self->{fields}{collection_performer} = $val if $field eq 'performer'; + } +} + +sub parse { + my ($self,$what) = @_; + return $self->return_parsed($what) if exists $self->{parsed}; + $self->parse_lines; + $self->{parsed} = { %{$self->{fields}} }; # Make a copy + $self->return_parsed($what); +} + +=pod + +=item title() + + $title = $db->title(); + +Returns the title, obtained from the C<'Tracktitle'> entry of the file. + +=cut + +# *song = \&title; + +sub title { + return shift->parse("title"); +} + +=pod + +=item artist() + + $artist = $db->artist(); + +Returns the artist name, obtained from the C<'Performer'> or +C<'Albumperformer'> entries (the first which is present) of the file. + +=cut + +sub artist { + return shift->parse("artist"); +} + +=pod + +=item track() + + $track = $db->track(); + +Returns the track number, stored during object creation, or queried from +the parent. + +=cut + +sub track { + my $self = shift; + return $self->{track} if defined $self->{track}; + return if $self->{recursive} or not $self->parent_ok; + local $self->{recursive} = 1; + return $self->{parent}->track1; +} + +=item year() + + $year = $db->year(); + +Returns the year, obtained from the C<'Year'> entry of the file. (Often +not present.) + +=cut + +sub year { + return shift->parse("year"); +} + +=pod + +=item album() + + $album = $db->album(); + +Returns the album name, obtained from the C<'Albumtitle'> entry of the file. + +=cut + +sub album { + return shift->parse("album"); +} + +=item comment() + + $comment = $db->comment(); + +Returns the C<'REM COMMENT'> entry of the file. (Often not present.) + +=cut + +sub comment { + return shift->parse("comment"); +} + +=item genre() + + $genre = $db->genre($filename); + +=cut + +sub genre { + return shift->parse("genre"); +} + +for my $elt ( qw( artist_collection songwriter ) ) { + no strict 'refs'; + *$elt = sub (;$) { + return shift->parse($elt); + } +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/File.pm b/fhem/FHEM/lib/MP3/Tag/File.pm new file mode 100644 index 000000000..e40fad09a --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/File.pm @@ -0,0 +1,481 @@ +package MP3::Tag::File; + +use strict; +use Fcntl; +use File::Basename; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::File - Module for reading / writing files + +=head1 SYNOPSIS + + my $mp3 = MP3::Tag->new($filename); + + ($title, $artist, $no, $album, $year) = $mp3->parse_filename(); + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::File is designed to be called from the MP3::Tag module. + +It offers possibilities to read/write data from files via read(), write(), +truncate(), seek(), tell(), open(), close(); one can find the filename via +the filename() method. + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + return undef unless -f $filename or -c $filename; + return bless {filename => $filename, parent => $parent}, $class; +} +*new = \&new_with_parent; # Obsolete handler + +# Destructor + +sub DESTROY { + my $self=shift; + if (exists $self->{FH} and defined $self->{FH}) { + $self->close; + } +} + +# File subs + +sub filename { shift->{filename} } + +sub open { + my $self=shift; + my $mode= shift; + if (defined $mode and $mode =~ /w/i) { + $mode=O_RDWR; # read/write mode + } else { + $mode=O_RDONLY; # read only mode + } + unless (exists $self->{FH}) { + local *FH; + if (sysopen (FH, $self->filename, $mode)) { + $self->{FH} = *FH; + binmode $self->{FH}; + } else { + warn "Open `" . $self->filename() . "' failed: $!\n"; + } + } + return exists $self->{FH}; +} + + +sub close { + my $self=shift; + if (exists $self->{FH}) { + close $self->{FH}; + delete $self->{FH}; + } +} + +sub write { + my ($self, $data) = @_; + if (exists $self->{FH}) { + local $\ = ''; + print {$self->{FH}} $data; + } +} + +sub truncate { + my ($self, $length) = @_; + if ($length<0) { + my @stat = stat $self->{FH}; + $length = $stat[7] + $length; + } + if (exists $self->{FH}) { + truncate $self->{FH}, $length; + } +} + +sub size { + my ($self) = @_; + return -s $self->{FH} if exists $self->{FH}; + return -s ($self->filename); +} + +sub seek { + my ($self, $pos, $whence)=@_; + $self->open unless exists $self->{FH}; + seek $self->{FH}, $pos, $whence; +} + +sub tell { + my ($self, $pos, $whence)=@_; + return undef unless exists $self->{FH}; + return tell $self->{FH}; +} + +sub read { + my ($self, $buf_, $length) = @_; + $self->open unless exists $self->{FH}; + return read $self->{FH}, $$buf_, $length; +} + +sub is_open { + return exists shift->{FH}; +} + +# keep the old name +*isOpen = \&is_open; + +# read and decode the header of the mp3 part of the file +# the raw content of the header fields is stored, the values +# are not interpreted in any way (e.g. layer==3 means 'Layer I' +# as specified in the mp3 format) +sub get_mp3_frame_header { + my ($self, $start) = @_; + + $start = 0 unless $start; + + if (exists $self->{mp3header}) { + return $self->{mp3header}; + } + + $self->seek($start, 0); + my ($data, $bits)=""; + while (1) { + my $nextdata; + $self->read(\$nextdata, 512); + return unless $nextdata; # no header found + $data .= $nextdata; + if ($data =~ /(\xFF[\xE0-\xFF]..)/) { + $bits = unpack("B32", $1); + last; + } + $data = substr $data, -3 + } + + my @fields; + for (qw/11 2 2 1 4 2 1 1 1 2 2 1 1 2/) { + push @fields, oct "0b" . substr $bits, 0, $_; + $bits = substr $bits, $_ if length $bits > $_; + } + + $self->{mp3header}={}; + for (qw/sync version layer proctection bitrate_id sampling_rate_id padding private + channel_mode mode_ext copyright original emphasis/) { + $self->{mp3header}->{$_}=shift @fields; + } + + return $self->{mp3header} +} + + +# use filename to determine information about song/artist/album + +=pod + +=over 4 + +=item parse_filename() + + ($title, $artist, $no, $album, $year) = $mp3->parse_filename($what, $filename); + +parse_filename() tries to extract information about artist, title, +track number, album and year from the filename. (For backward +compatibility it may be also called by deprecated name +read_filename().) + +This is likely to fail for a lot of filenames, especially the album will +be often wrongly guessed, as the name of the parent directory is taken as +album name. + +$what and $filename are optional. $what maybe title, track, artist, album +or year. If $what is defined parse_filename() will return only this element. + +If $filename is defined this filename will be used and not the real +filename which was set by L<MP3::Tag> with +C<MP3::Tag-E<gt>new($filename)>. Otherwise the actual filename is used +(subject to configuration variable C<decode_encoding_filename>). + +Following formats will be hopefully recognized: + +- album name/artist name - song name.mp3 + +- album_name/artist_name-song_name.mp3 + +- album.name/artist.name_song.name.mp3 + +- album name/(artist name) song name.mp3 + +- album name/01. artist name - song name.mp3 + +- album name/artist name - 01 - song.name.mp3 + +If artist or title end in C<(NUMBER)> with 4-digit NUMBER, it is considered +the year. + +=cut + +*read_filename = \&parse_filename; + +sub return_parsed { + my ($self,$what) = @_; + if (defined $what) { + return $self->{parsed}{album} if $what =~/^al/i; + return $self->{parsed}{artist} if $what =~/^a/i; + return $self->{parsed}{no} if $what =~/^tr/i; + return $self->{parsed}{year} if $what =~/^y/i; + return $self->{parsed}{title}; + } + + return $self->{parsed} unless wantarray; + return map $self->{parsed}{$_} , qw(title artist no album year); +} + +sub parse_filename { + my ($self,$what,$filename) = @_; + unless (defined $filename) { + $filename = $self->filename; + my $e; + if ($e = $self->get_config('decode_encoding_filename') and $e->[0]) { + require Encode; + $filename = Encode::decode($e->[0], $filename); + } + } + my $pathandfile = $filename; + + $self->return_parsed($what) if exists $self->{parsed_filename} + and $self->{parsed_filename} eq $filename; + + # prepare pathandfile for easier use + my $ext_rex = $self->get_config('extension')->[0]; + $pathandfile =~ s/$ext_rex//; # remove extension + $pathandfile =~ s/ +/ /g; # replace several spaces by one space + + # Keep two last components of the file name + my ($file, $path) = fileparse($pathandfile, ""); + ($path) = fileparse($path, ""); + my $orig_file = $file; + + # check which chars are used for seperating words + # assumption: spaces between words + + unless ($file =~/ /) { + # no spaces used, find word seperator + my $Ndot = $file =~ tr/././; + my $Nunderscore = $file =~ tr/_/_/; + my $Ndash = $file =~ tr/-/-/; + if (($Ndot>$Nunderscore) && ($Ndot>1)) { + $file =~ s/\./ /g; + } + elsif ($Nunderscore > 1) { + $file =~ s/_/ /g; + } + elsif ($Ndash>2) { + $file =~ s/-/ /g; + } + } + + # check wich chars are used for seperating parts + # assumption: " - " is used + + my $partsep = " - "; + + unless ($file =~ / - /) { + if ($file =~ /-/) { + $partsep = "-"; + } elsif ($file =~ /^\(.*\)/) { + # replace brackets by - + $file =~ s/^\((.*?)\)/$1 - /; + $file =~ s/ +/ /; + $partsep = " - "; + } elsif ($file =~ /_/) { + $partsep = "_"; + } else { + $partsep = "DoesNotExist"; + } + } + + # get parts of name + my ($title, $artist, $no, $album, $year)=("","","","",""); + + # try to find a track-number in front of filename + if ($file =~ /^ *(\d+)[\W_]/) { + $no=$1; # store number + $file =~ s/^ *\d+//; # and delete it + $file =~ s/^$partsep// || $file =~ s/^.//; + $file =~ s/^ +//; + } + + $file =~ s/_+/ /g unless $partsep =~ /_/; #remove underscore unless they are needed for part seperation + my @parts = split /$partsep/, $file; + if (@parts == 1) { + $title=$parts[0]; + $no = $file if $title and $title =~ /^\d{1,2}$/; + } elsif (@parts == 2) { + if ($parts[0] =~ /^\d{1,2}$/) { + $no = $parts[0]; + $title = $file; + } elsif ($parts[1] =~ /^\d{1,2}$/) { + $no = $parts[1]; + $title = $file; + } else { + $artist=$parts[0]; + $title=$parts[1]; + } + } elsif (@parts > 2) { + my $temp = ""; + $artist = shift @parts; + foreach (@parts) { + if (/^ *(\d+)\.? *$/) { + $artist.= $partsep . $temp if $temp; + $temp=""; + $no=$1; + } else { + $temp .= $partsep if $temp; + $temp .= $_; + } + } + $title=$temp; + } + + $title =~ s/ +$//; + $artist =~ s/ +$//; + $no =~ s/ +$//; + + # Special-case names like audio12 etc created by some software + # (cdda2wav, gramofile, etc) + $no = $+ if not $no and $title =~ /^(\d+)?(?:audio|track|processed)\s*(\d+)?$/i and $+; + + $no =~ s/^0+//; + + if ($path) { + unless ($artist) { + $artist = $path; + } else { + $album = $path; + } + } + # Keep the year in the title/artist (XXXX Should we?) + $year = $1 if $title =~ /\((\d{4})\)/ or $artist =~ /\((\d{4})\)/; + + $self->{parsed_filename} = $filename; + $self->{parsed} = { artist=>$artist, song=>$title, no=>$no, + album=>$album, title=>$title, year => $year}; + $self->return_parsed($what); +} + + +=pod + +=item title() + + $title = $mp3->title($filename); + +Returns the title, guessed from the filename. See also parse_filename(). (For +backward compatibility, can be called by deprecated name song().) + +$filename is optional and will be used instead of the real filename if defined. + +=cut + +*song = \&title; + +sub title { + my $self = shift; + return $self->parse_filename("title", @_); +} + +=pod + +=item artist() + + $artist = $mp3->artist($filename); + +Returns the artist name, guessed from the filename. See also parse_filename() + +$filename is optional and will be used instead of the real filename if defined. + +=cut + +sub artist { + my $self = shift; + return $self->parse_filename("artist", @_); +} + +=pod + +=item track() + + $track = $mp3->track($filename); + +Returns the track number, guessed from the filename. See also parse_filename() + +$filename is optional and will be used instead of the real filename if defined. + +=cut + +sub track { + my $self = shift; + return $self->parse_filename("track", @_); +} + +=item year() + + $year = $mp3->year($filename); + +Returns the year, guessed from the filename. See also parse_filename() + +$filename is optional and will be used instead of the real filename if defined. + +=cut + +sub year { + my $self = shift; + my $y = $self->parse_filename("year", @_); + return $y if length $y; + return; +} + +=pod + +=item album() + + $album = $mp3->album($filename); + +Returns the album name, guessed from the filename. See also parse_filename() +The album name is guessed from the parent directory, so it is very likely to fail. + +$filename is optional and will be used instead of the real filename if defined. + +=cut + +sub album { + my $self = shift; + return $self->parse_filename("album", @_); +} + +=item comment() + + $comment = $mp3->comment($filename); # Always undef + +=cut + +sub comment {} + +=item genre() + + $genre = $mp3->genre($filename); # Always undef + +=cut + +sub genre {} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/ID3v1.pm b/fhem/FHEM/lib/MP3/Tag/ID3v1.pm new file mode 100644 index 000000000..851546f24 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ID3v1.pm @@ -0,0 +1,544 @@ +package MP3::Tag::ID3v1; + +# Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Artistic License, distributed +# with Perl. + +use strict; +use vars qw /@mp3_genres @winamp_genres $AUTOLOAD %ok_length $VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +# allowed fields in ID3v1.1 and max length of this fields (except for track and genre which are coded later) +%ok_length = (title => 30, artist => 30, album => 30, comment => 28, track => 3, genre => 3000, year=>4, genreID=>1); + +=pod + +=head1 NAME + +MP3::Tag::ID3v1 - Module for reading / writing ID3v1 tags of MP3 audio files + +=head1 SYNOPSIS + +MP3::Tag::ID3v1 is designed to be called from the MP3::Tag module. + + use MP3::Tag; + $mp3 = MP3::Tag->new($filename); + + # read an existing tag + $mp3->get_tags(); + $id3v1 = $mp3->{ID3v1} if exists $mp3->{ID3v1}; + + # or create a new tag + $id3v1 = $mp3->new_tag("ID3v1"); + +See L<MP3::Tag|according documentation> for information on the above used functions. + +* Reading the tag + + print " Title: " .$id3v1->title . "\n"; + print " Artist: " .$id3v1->artist . "\n"; + print " Album: " .$id3v1->album . "\n"; + print "Comment: " .$id3v1->comment . "\n"; + print " Year: " .$id3v1->year . "\n"; + print " Genre: " .$id3v1->genre . "\n"; + print " Track: " .$id3v1->track . "\n"; + + # or at once + @tagdata = $mp3->all(); + foreach $tag (@tagdata) { + print $tag; + } + +* Changing / Writing the tag + + $id3v1->comment("This is only a Test Tag"); + $id3v1->title("testing"); + $id3v1->artist("Artest"); + $id3v1->album("Test it"); + $id3v1->year("1965"); + $id3v1->track("5"); + $id3v1->genre("Blues"); + # or at once + $id3v1->all("song title","artist","album","1900","comment",10,"Ska"); + $id3v1->write_tag(); + +* Removing the tag from the file + + $id3v1->remove_tag(); + +=head1 AUTHOR + +Thomas Geffert, thg@users.sourceforge.net + +=head1 DESCRIPTION + +=pod + +=over + +=item title(), artist(), album(), year(), comment(), track(), genre() + + $artist = $id3v1->artist; + $artist = $id3v1->artist($artist); + $album = $id3v1->album; + $album = $id3v1->album($album); + $year = $id3v1->year; + $year = $id3v1->year($year); + $comment = $id3v1->comment; + $comment = $id3v1->comment($comment); + $track = $id3v1->track; + $track = $id3v1->track($track); + $genre = $id3v1->genre; + $genre = $id3v1->genre($genre); + +Use these functions to retrieve the date of these fields, +or to set the data. + +$genre can be a string with the name of the genre, or a number +describing the genre. + +=cut + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + + # is it an allowed field + $attr =~ s/.*:://; + return unless $attr =~ /[^A-Z]/; + $attr = 'title' if $attr eq 'song'; + warn "invalid field: ->$attr()" unless $ok_length{$attr}; + + if (@_) { + my $new = shift; + $new =~ s/ *$//; + if ($attr eq "genre") { + if ($new =~ /^\d+$/) { + $self->{genreID} = $new; + } else { + $self->{genreID} = genre2id($new); + } + $new = id2genre($self->{genreID}) + if defined $self->{genreID} and $self->{genreID} < @winamp_genres; + } + $new = substr $new, 0, $ok_length{$attr}; + $self->{$attr}=$new; + $self->{changed} = 1; + } + $self->{$attr} =~ s/ +$//; + return $self->{$attr}; +} + +=pod + +=item all() + + @tagdata = $id3v1->all; + @tagdata = $id3v1->all($title, $artist, $album, $year, $comment, $track, $genre); + +Returns all information of the tag in a list. +You can use this sub also to set the data of the complete tag. + +The order of the data is always title, artist, album, year, comment, track, and genre. +genre has to be a string with the name of the genre, or a number identifying the genre. + +=cut + +sub all { + my $self=shift; + if ($#_ == 6) { + my $new; + for (qw/title artist album year comment track genre/) { + $new = shift; + $new =~ s/ +$//; + $new = substr $new, 0, $ok_length{$_}; + $self->{$_}=$new; + } + if ($self->{genre} =~ /^\d+$/) { + $self->{genreID} = $self->{genre}; + } else { + $self->{genreID} = genre2id($self->{genre}); + } + $self->{genre} = id2genre($self->{genreID}) + if defined $self->{genreID} and $self->{genreID} < @winamp_genres; + $self->{changed} = 1; + } + for (qw/title artist album year comment track genre/) { + $self->{$_} =~ s/ +$//; + } + if (wantarray) { + return ($self->{title},$self->{artist},$self->{album}, + $self->{year},$self->{comment}, $self->{track}, $self->{genre}); + } + return $self->{title}; +} + +=pod + +=item fits_tag() + + warn "data truncated" unless $id3v1->fits_tag($hash); + +Check whether the info in ID3v1 tag fits into the format of the file. + +=cut + +sub fits_tag { + my ($self, $hash) = (shift, shift); + my $elt; + if (defined (my $track = $hash->{track})) { + $track = $track->[0] if ref $track; + return unless $track =~ /^\d{0,3}$/ and ($track eq '' or $track < 256); + } + my $s = ''; + for $elt (qw(title artist album comment year)) { + next unless defined (my $data = $hash->{$elt}); + $data = $data->[0] if ref $data; + return if $data =~ /[^\x00-\xFF]/; + $s .= $data; + next if $ok_length{$elt} >= length $data; + next + if $elt eq 'comment' and not $hash->{track} and length $data <= 30; + return; + } + if (defined (my $genre = $hash->{genre})) { + $genre = $genre->[0] if ref $genre; + my @g = MP3::Tag::Implemenation::_massage_genres($genre); + return if @g > 1; + my $id = MP3::Tag::Implemenation::_massage_genres($genre, 'num'); + return if not defined $id or $id eq '' or $id == 255; + } + if ($s =~ /[^\x00-\x7E]/) { + my $w = ($self->get_config('encode_encoding_v1') || [0])->[0]; + my $r = ($self->get_config('decode_encoding_v1') || [0])->[0]; + $_ = (lc or 'iso-8859-1') for $r, $w; + # Safe: per-standard and read+write is idempotent: + return 1 if $r eq $w and $w eq 'iso-8859-1'; + return !(($self->get_config('encoded_v1_fits')||[0])->[0]) + if $w eq 'iso-8859-1'; # read+write not idempotent + return if $w ne $r + and not (($self->get_config('encoded_v1_fits')||[0])->[0]); + } + return 1; +} + +=item as_bin() + + $str = $id3v1->as_bin(); + +Returns the ID3v1 tag as a string. + +=item write_tag() + + $id3v1->write_tag(); + + [old name: writeTag() . The old name is still available, but you should use the new name] + +Writes the ID3v1 tag to the file. + +=cut + +sub as_bin { + my $self = shift; + my($t) = ( $self->{track} =~ m[^(\d+)(?:/|$)], 0 ); + my (%f, $f, $e); + for $f (qw(title artist album comment) ) { + $f{$f} = $self->{$f}; + } + + if ($e = $self->get_config('encode_encoding_v1') and $e->[0]) { + my $field; + require Encode; + + for $field (qw(title artist album comment)) { + $f{$field} = Encode::encode($e->[0], $f{$field}); + } + } + + $f{comment} = pack "a28 x C", $f{comment}, $t if $t; + $self->{genreID}=255 unless $self->{genreID} =~ /^\d+$/; + + return pack("a3a30a30a30a4a30C","TAG",$f{title}, $f{artist}, + $f{album}, $self->{year}, $f{comment}, $self->{genreID}); +} + +sub write_tag { + my $self = shift; + return undef unless exists $self->{title} && exists $self->{changed}; + my $data = $self->as_bin(); + my $mp3obj = $self->{mp3}; + my $mp3tag; + $mp3obj->close; + if ($mp3obj->open("write")) { + $mp3obj->seek(-128,2); + $mp3obj->read(\$mp3tag, 3); + if ($mp3tag eq "TAG") { + $mp3obj->seek(-125,2); # neccessary for windows + $mp3obj->write(substr $data, 3); + } else { + $mp3obj->seek(0,2); + $mp3obj->write($data); + } + } else { + warn "Couldn't open file `" . $mp3obj->filename() . "' to write tag"; + return 0; + } + return 1; +} + +*writeTag = \&write_tag; + +=pod + +=item remove_tag() + + $id3v1->remove_tag(); + +Removes the ID3v1 tag from the file. Returns negative on failure, +FALSE if no tag was found. + +(Caveat: only I<one tag> is removed; some - broken - files may have +many chain-loaded one after another; you may need to call remove_tag() +in a loop to handle such beasts.) + +[old name: removeTag() . The old name is still available, but you +should use the new name] + +=cut + +sub remove_tag { + my $self = shift; + my $mp3obj = $self->{mp3}; + my $mp3tag; + $mp3obj->seek(-128,2); + $mp3obj->read(\$mp3tag, 3); + if ($mp3tag eq "TAG") { + $mp3obj->close; + if ($mp3obj->open("write")) { + $mp3obj->truncate(-128); + $self->all("","","","","",0,255); + $mp3obj->close; + $self->{changed} = 1; + return 1; + } + return -1; + } + return 0; +} + +*removeTag = \&remove_tag; + +=pod + +=item genres() + + @allgenres = $id3v1->genres; + $genreName = $id3v1->genres($genreID); + $genreID = $id3v1->genres($genreName); + +Returns a list of all genres, or the according name or id to +a given id or name. + +=cut + +sub genres { + # return an array with all genres, of if a parameter is given, the according genre + my ($self, $genre) = @_; + if ( (defined $self) and (not defined $genre) and ($self !~ /MP3::Tag/)) { + ## genres may be called directly via MP3::Tag::ID3v1::genres() + ## and $self is then not used for an id3v1 object + $genre = $self; + } + + return \@winamp_genres unless defined $genre; + + if ($genre =~ /^\d+$/) { + return $winamp_genres[$genre] if $genre<scalar @winamp_genres; + return undef; + } + + my ($id, $found)=0; + foreach (@winamp_genres) { + if (uc $_ eq uc $genre) { + $found = 1; + last; + } + $id++; + } + $id=255 unless $found; + return $id; +} + +=item new() + + $id3v1 = MP3::Tag::ID3v1->new($mp3fileobj[, $create]); + +Generally called from MP3::Tag, because a $mp3fileobj is needed. +If $create is true, a new tag is created. Otherwise undef is +returned, if now ID3v1 tag is found in the $mp3obj. + +Please use + + $mp3 = MP3::Tag->new($filename); + $id3v1 = $mp3->new_tag("ID3v1"); # Empty new tag + +or + + $mp3 = MP3::Tag->new($filename); + $mp3->get_tags(); + $id3v1 = $mp3->{ID3v1}; # Existing tag (if present) + +instead of using this function directly + +=back + +=cut + +# create a ID3v1 object +sub new { + my ($class, $fileobj, $create) = @_; + my $self={mp3=>$fileobj}; + my $buffer; + + if ($create) { + $self->{new} = 1; + } else { + $fileobj->open or return unless $fileobj->is_open; + $fileobj->seek(-128,2); + $fileobj->read(\$buffer, 128); + return undef unless substr ($buffer,0,3) eq "TAG"; + } + + bless $self, $class; + $self->read_tag($buffer); # $buffer unused if ->{new} + return $self; +} + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + return unless my $new = $class->new($filename, undef); + $new->{parent} = $parent; + $new; +} + +################# +## +## internal subs + +# actually read the tag data +sub read_tag { + my ($self, $buffer) = @_; + my ($id3v1, $e); + + if ($self->{new}) { + ($self->{title}, $self->{artist}, $self->{album}, $self->{year}, + $self->{comment}, $self->{track}, $self->{genre}, $self->{genreID}) = ("","","","","",'',"",255); + $self->{changed} = 1; + } else { + (undef, $self->{title}, $self->{artist}, $self->{album}, $self->{year}, + $self->{comment}, $id3v1, $self->{track}, $self->{genreID}) = + unpack (($] < 5.6 + ? "a3 A30 A30 A30 A4 A28 C C C" # Trailing spaces stripped too + : "a3 Z30 Z30 Z30 Z4 Z28 C C C"), + $buffer); + + if ($id3v1!=0) { # ID3v1 tag found: track is not valid, comment two chars longer + $self->{comment} .= chr($id3v1); + $self->{comment} .= chr($self->{track}) + if $self->{track} and $self->{track}!=32; + $self->{track} = ''; + }; + $self->{track} = '' unless $self->{track}; + $self->{genre} = id2genre($self->{genreID}); + if ($e = $self->get_config('decode_encoding_v1') and $e->[0]) { + my $field; + require Encode; + + for $field (qw(title artist album comment)) { + $self->{$field} = Encode::decode($e->[0], $self->{$field}); + } + } + } +} + +# convert small integer id to genre name +sub id2genre { + my $id=shift; + return "" unless defined $id and $id < @winamp_genres; + return $winamp_genres[$id]; +} + +# convert genre name to small integer id +sub genre2id { + my $genre = MP3::Tag::Implemenation::_massage_genres(shift, 'num'); + return $genre if defined $genre; + return 255; +} + +# nothing to do for destroy +sub DESTROY { +} + +1; + +######## define all the genres + +BEGIN { @mp3_genres = ( 'Blues', 'Classic Rock', 'Country', 'Dance', + 'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', + 'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', + 'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', + 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', + 'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', + 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', + 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', + 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave', + 'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream', + 'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', + 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave', + 'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', + 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', + 'Hard Rock', ); + + @winamp_genres = ( @mp3_genres, 'Folk', 'Folk-Rock', + 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival', + 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', + 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', + 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', + 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', + 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', + 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', + 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', + 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'Acapella', + 'Euro-House', 'Dance Hall', + # More from MP3::Info + 'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', + 'Terror', 'Indie', 'BritPop', 'Negerpunk', + 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', + 'Heavy Metal', 'Black Metal', 'Crossover', + 'Contemporary Christian Music', 'Christian Rock', + 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', + 'JPop', 'SynthPop', # 149 + ); +} + +=pod + +=head1 SEE ALSO + +L<MP3::Tag>, L<MP3::Tag::ID3v2> + +ID3v1 standard - http://www.id3.org + +=head1 COPYRIGHT + +Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. + +This program is free software; you can redistribute it and/or +modify it under the terms of the Artistic License, distributed +with Perl. + +=cut diff --git a/fhem/FHEM/lib/MP3/Tag/ID3v2.pm b/fhem/FHEM/lib/MP3/Tag/ID3v2.pm new file mode 100644 index 000000000..7d0bcc4f6 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ID3v2.pm @@ -0,0 +1,2989 @@ +package MP3::Tag::ID3v2; + +# Copyright (c) 2000-2004 Thomas Geffert. All rights reserved. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Artistic License, distributed +# with Perl. + +use strict; +use File::Basename; +# use Compress::Zlib; + +use vars qw /%format %long_names %res_inp @supported_majors %v2names_to_v3 + $VERSION @ISA %field_map %field_map_back %is_small_int + %back_splt %embedded_Descr + /; + +$VERSION = "1.12"; +@ISA = 'MP3::Tag::__hasparent'; + +my $trustencoding = $ENV{MP3TAG_DECODE_UNICODE}; +$trustencoding = 1 unless defined $trustencoding; + +my $decode_utf8 = $ENV{MP3TAG_DECODE_UTF8}; +$decode_utf8 = 1 unless defined $decode_utf8; +my $encode_utf8 = $decode_utf8; + +=pod + +=head1 NAME + +MP3::Tag::ID3v2 - Read / Write ID3v2.x.y tags from mp3 audio files + +=head1 SYNOPSIS + +MP3::Tag::ID3v2 supports + * Reading of ID3v2.2.0 and ID3v2.3.0 tags (some ID3v2.4.0 frames too) + * Writing of ID3v2.3.0 tags + +MP3::Tag::ID3v2 is designed to be called from the MP3::Tag module. If +you want to make calls from user code, please consider using +highest-level wrapper code in MP3::Tag, such as update_tags() and +select_id3v2_frame_by_descr(). + +Low-level creation code: + + use MP3::Tag; + $mp3 = MP3::Tag->new($filename); + + # read an existing tag + $mp3->get_tags(); + $id3v2 = $mp3->{ID3v2} if exists $mp3->{ID3v2}; + + # or create a new tag + $id3v2 = $mp3->new_tag("ID3v2"); + +See L<MP3::Tag|according documentation> for information on the above used functions. + +* Reading a tag, very low-level: + + $frameIDs_hash = $id3v2->get_frame_ids('truename'); + + foreach my $frame (keys %$frameIDs_hash) { + my ($name, @info) = $id3v2->get_frames($frame); + for my $info (@info) { + if (ref $info) { + print "$name ($frame):\n"; + while(my ($key,$val)=each %$info) { + print " * $key => $val\n"; + } + } else { + print "$name: $info\n"; + } + } + } + +* Adding / Changing / Removing a frame in memory (higher-level) + + $t = $id3v2->frame_select("TIT2", undef, undef); # Very flexible + + $c = $id3v2->frame_select_by_descr("COMM(fre,fra,eng,#0)[]"); + $t = $id3v2->frame_select_by_descr("TIT2"); + $id3v2->frame_select_by_descr("TIT2", "MyT"); # Set/Change + $id3v2->frame_select_by_descr("RBUF", $n1, $n2, $n3); # Set/Change + $id3v2->frame_select_by_descr("RBUF", "$n1;$n2;$n3"); # Set/Change + $id3v2->frame_select_by_descr("TIT2", undef); # Remove + +* Adding / Changing / Removing a frame in memory (low-level) + + $id3v2->add_frame("TIT2", "Title of the audio"); + $id3v2->change_frame("TALB","Greatest Album"); + $id3v2->remove_frame("TLAN"); + +* Output the modified-in-memory version of the tag: + + $id3v2->write_tag(); + +* Removing the whole tag from the file + + $id3v2->remove_tag(); + +* Get information about supported frames + + %tags = $id3v2->supported_frames(); + while (($fname, $longname) = each %tags) { + print "$fname $longname: ", + join(", ", @{$id3v2->what_data($fname)}), "\n"; + } + +=head1 AUTHOR + +Thomas Geffert, thg@users.sourceforge.net +Ilya Zakharevich, ilyaz@cpan.org + +=head1 DESCRIPTION + +=over 4 + +=item get_frame_ids() + + $frameIDs = $tag->get_frame_ids; + $frameIDs = $tag->get_frame_ids('truename'); + + [old name: getFrameIDs() . The old name is still available, but you should use the new name] + +get_frame_ids loops through all frames, which exist in the tag. It +returns a hash reference with a list of all available Frame IDs. The +keys of the returned hash are 4-character-codes (short names), the +internal names of the frames, the according value is the english +(long) name of the frame. + +You can use this list to iterate over all frames to get their data, or to +check if a specific frame is included in the tag. + +If there are multiple occurences of a frame in one tag, the first frame is +returned with its normal short name, following frames of this type get a +'01', '02', '03', ... appended to this name. These names can then +used with C<get_frame> to get the information of these frames. These +fake frames are not returned if C<'truename'> argument is set; one +can still use C<get_frames()> to extract the info for all of the frames with +the given short name. + +=cut + +###### structure of a tag frame +# +# major=> Identifies format of frame, normally set to major version of the whole +# tag, but many ID3v2.2 frames are converted automatically to ID3v2.3 frames +# flags=> Frame flags, depend on major version +# data => Data of frame, including gid +# gid => group id, if any (created by get_frame()) +# + +sub un_syncsafe_4bytes ($) { + my ($rawsize,$size) = (shift, 0); + foreach my $b (unpack("C4", $rawsize)) { + $size = ($size << 7) + $b; + } + return $size; +} + +sub get_frame_ids { + my $self = shift; # Tag + my $basic = shift; + + # frame headers format for the different majors + my $headersize = (0,0,6,10,10)[$self->{major}]; + my $headerformat=("","","a3a3","a4Nn","a4a4n")[$self->{major}]; + + if (exists $self->{frameIDs}) { + return unless defined wantarray; + my %return; + foreach (keys %{$self->{frames}}) { + next if $basic and length > 4; # ignore frames with 01 etc. at end + $return{$_}=$long_names{substr($_,0,4)}; + } + return \%return; + } + + my $pos = $self->{frame_start}; +# if ($self->{flags}->{extheader}) { +# warn "get_frame_ids: possible wrong IDs because of unsupported extended header\n"; +# } + my $buf; + while ($pos + $headersize < $self->{data_size}) { + $buf = substr ($self->{tag_data}, $pos, $headersize); + my ($ID, $size, $flags) = unpack($headerformat, $buf); + # tag size is handled differently for all majors + if ($self->{major} == 2) { + # flags don't exist in id3v2.2 + $flags=0; + my $rawsize=$size; + $size=0; + foreach (unpack("C3", $rawsize)) { + $size = ($size << 8) + $_; + } + } elsif ($self->{major} == 4) { + $size = un_syncsafe_4bytes $size; + } elsif ($self->{major}==3 and $size>255) { + # Size>255 means at least 2 bytes are used for size. + # Some programs use (incorectly) for the frame size + # the format of the tag size (snchsafe). Trying do detect that here + if ($pos + $headersize + $size > $self->{data_size} || + !exists $long_names{substr ($self->{tag_data}, $pos+$size,4)}) { + # wrong size or last frame + my $fsize = un_syncsafe_4bytes substr $buf, 4, 4; + if ($pos + 20 + $fsize < $self->{data_size} && + exists $long_names{substr ($self->{tag_data}, $pos+10+$fsize,4)}) { + warn "Probably wrong size format found in frame $ID. Trying to correct it\n"; + #probably false size format detected, using corrected size + $size = $fsize; + } + } + } + + if ($ID !~ "\000\000\000") { + my $major = $self->{major}; + if ($major == 2) { + # most frame IDs can be converted directly to id3v2.3 IDs + if (exists $v2names_to_v3{$ID}) { + # frame is direct convertable to major 3 + $ID = $v2names_to_v3{$ID}; + $major=3; + } + } + if (exists $self->{frames}->{$ID}) { + ++$self->{extra_frames}->{$ID}; + $ID .= '01'; + while (exists $self->{frames}->{$ID}) { + $ID++; + } + } + + $self->{frames}->{$ID} = {flags=>$self->check_flags($flags), + major=>$major, + data=>substr($self->{tag_data}, $pos+$headersize, $size)}; + $pos += $size+$headersize; + } else { # Padding reached, cut tag data here + last; + } + } + $self->{endpos} = $pos; + # Since tag_data is de-synced, this doesn't count the forced final "\0" + $self->{padding} = length($self->{tag_data}) - $pos; + # tag is seperated into frames, tagdata not more needed + $self->{tag_data}=""; + + $self->{frameIDs} =1; + my %return; + foreach (keys %{$self->{frames}}) { + next if $basic and length > 4; # ignore frames with 01 etc. at end + $return{$_}=$long_names{substr($_,0,4)}; + } + return \%return; +} + +*getFrameIDs = \&get_frame_ids; + +=pod + +=item get_frame() + + ($info, $name, @rest) = $tag->get_frame($ID); + ($info, $name, @rest) = $tag->get_frame($ID, 'raw'); + + [old name: getFrame() . The old name is still available, but you should use the new name] + +get_frame gets the contents of a specific frame, which must be specified by the +4-character-ID (aka short name). You can use C<get_frame_ids> to get the IDs of +the tag, or use IDs which you hope to find in the tag. If the ID is not found, +C<get_frame> returns empty list, so $info and $name become undefined. + +Otherwise it extracts the contents of the frame. Frames in ID3v2 tags can be +very small, or complex and huge. That is the reason, that C<get_frame> returns +the frame data in two ways, depending on the tag. + +If it is a simple tag, with only one piece of data, these data is returned +directly as ($info, $name), where $info is the text string, and $name is the +long (english) name of the frame. + +If the frame consist of different pieces of data, $info is a hash reference, +$name is again the long name of the frame. + +The hash, to which $info points, contains key/value pairs, where the key is +always the name of the data, and the value is the data itself. + +If the name starts with a underscore (as eg '_code'), the data is probably +binary data and not printable. If the name starts without an underscore, +it should be a text string and printable. + +If the second parameter is given as C<'raw'>, the whole frame data is returned, +but not the frame header. If the second parameter is C<'intact'>, no mangling +of embedded C<"\0"> and trailing spaces is performed. If the second parameter +is C<'hash'>, then, additionally, the result is always in the hash format; +likewise, if it is C<'array'>, the result is an array reference (with C<key +=E<gt> value> pairs same as with C<'hash'>, but ordered as in the frame). +If it is C<'array_nokey'>, only the "value" parts are returned (in particular, +the result is suitable to give to add_frame(), change_frame()); in addition, +if it is C<'array_nodecode'>, then keys are not returned, and the setting of +C<decode_encoding_v2> is ignored. (The "return array" flavors don't massage +the fields for better consumption by humans, so the fields should be in format +suitable for frame_add().) + +If the data was stored compressed, it is +uncompressed before it is returned (even in raw mode). Then $info contains a string +with all data (which might be binary), and $name the long frame name. + +See also L<MP3::Tag::ID3v2_Data> for a list of all supported frames, and +some other explanations of the returned data structure. + +If more than one frame with name $ID is present, @rest contains $info +fields for all consequent frames with the same name. Note that after +removal of frames there may be holes in the list of frame names (as in +C<FRAM FRAM01 FRAM02>) in the case when multiple frames of the given +type were present; the removed frames are returned as C<undef>. + +! Encrypted frames are not supported yet ! + +! Some frames are not supported yet, but the most common ones are supported ! + +=cut + +sub get_frame { + my ($self, $fname, $raw) = @_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + my ($e, @extra) = 0; # More frames follow? + $e = $self->{extra_frames}->{$fname} || 0 + if wantarray and $self->{extra_frames} and length $fname == 4; + @extra = map scalar $self->get_frame((sprintf "%s%02d", $fname, $_), $raw), + 1..$e; + $e = grep defined, @extra; + my $frame = $self->{frames}->{$fname}; + return unless defined $frame or $e; + $fname = substr ($fname, 0, 4); + return (undef, $long_names{$fname}, @extra) unless defined $frame; + my $start_offset=0; + if ($frame->{flags}->{encryption}) { + warn "Frame $fname: encryption not supported yet\n" ; + return; + } + + my $result = $frame->{data}; + +# Some frame format flags indicate that additional information fields +# are added to the frame. This information is added after the frame +# header and before the frame data in the same order as the flags that +# indicates them. I.e. the four bytes of decompressed size will precede +# the encryption method byte. These additions affects the 'frame size' +# field, but are not subject to encryption or compression. + if ($frame->{flags}->{groupid}) { + $frame->{gid} = substring $result, 0, 1; + $result = substring $result, 1; + } + + if ($frame->{flags}->{compression}) { + my $usize=unpack("N", $result); + require Compress::Zlib; + $result = Compress::Zlib::uncompress(substr ($result, 4)); + warn "$fname: Wrong size of uncompressed data\n" if $usize=!length($result); + } + + if (($raw ||= 0) eq 'raw') { + return ($result, $long_names{$fname}, @extra) if wantarray; + return $result; + } + + my $format = get_format($fname); + if (defined $format) { + my($as_arr, $nodecode); + $as_arr = 2 if $raw eq 'array'; + $as_arr = 1 if $raw eq 'array_nokey' or $raw eq 'array_nodecode'; + $nodecode = 1 if $raw eq 'array_nodecode'; + $format = [map +{%$_}, @$format], $format->[-1]{data} = 1 + if $raw eq 'intact' or $raw eq 'hash' or $as_arr; + $result = extract_data($self, $result, $format, $nodecode, $as_arr); + unless ($as_arr or $raw eq 'hash') { + my $k = scalar keys %$result; + $k-- if exists $result->{encoding}; + if ($k == 1) { + if (exists $result->{Text}) { + $result = $result->{Text}; + } elsif (exists $result->{URL}) { + $result = $result->{URL}; + } elsif ($fname =~ /^MCDI/) { # Per ID3v2_Data.pod + $result = $result->{_Data}; + } # In fact, no other known frame has one element + } + } + } + if (wantarray) { + return ($result, $long_names{$fname}, @extra); + } else { + return $result; + } +} + +*getFrame= \&get_frame; + +=item get_frame_descr() + + $long_name = $self->get_frame_descr($fname); + +returns a "long name" for the frame (such as C<COMM(eng)[lyricist birthdate]>), +appropriate for interpolation, or for frame_select_by_descr(). + +=item get_frame_descriptors() + + @long_names = $self->get_frame_descriptors(); + +return "long names" for the frames in the tag (see C<get_frame_descr>). + +=cut + +sub get_frame_descr { + my ($self, $fname)=@_; + (undef, my $frame) = $self->get_frames($fname); # Ignore the rest + return unless defined $frame; + return $fname unless ref $frame; + my $k = scalar keys %$frame; + if ($k == 5 and substr($fname, 0, 4) eq 'APIC') { + return $fname unless + $frame->{'MIME type'} eq $self->_Data_to_MIME($frame->{_Data}); + delete $frame->{'MIME type'}; + $k--; + $frame->{Language} = delete $frame->{'Picture Type'}; + } + return $fname unless $k <= 4; # encoding, Language, Description + 1 + $k-- if exists $frame->{encoding}; + return $fname unless $k <= 3; + my $l = delete $frame->{Language}; + $k-- if defined $l; + return $fname unless $k <= 2; + my $d = delete $frame->{Description}; + $k-- if defined $d; + return $fname unless $k <= 1; + $fname =~ s/^(\w{4})\d{2}/$1/; + $l = "($l)" if defined $l; + $d = "[$d]" if defined $d; + $l ||= ''; + $d ||= ''; + return "$fname$l$d"; +} + +sub get_frame_descriptors { + my $self = shift; + my $h = $self->get_frame_ids(); + map $self->get_frame_descr($_), sort keys %$h; +} + +# I'm not yet ready to freeze these APIs +sub __frame_as_printable { + my ($self,$descr,$pre,$post,$fsep,$pre_mult,$val_sep, $bin) = (shift, shift, shift, shift, shift, shift, shift, shift); + my $val = $self->frame_select_by_descr($descr); + # Simple binary frames: + my $l = length $val; + return '__binary_DATA__ [len='.length($val).']' + if not $bin and not ref $val and $descr =~ /^(MCDI|APIC)/; + return "$pre$val$post" unless ref $val; + my $format = get_format(substr $descr, 0, 4); + my %optnl = map(($_->{name},$_->{optional}), @$format); + my @keys = map $_->{name}, @$format; # In order... + s/^_(?=encoding$)// for @keys; # Reverse mangling by extract_data()... + my %keys = map(($_,1), @keys); + my @ekeys = grep !exists $keys{$_}, keys %$val; # Just in case... + my @miss = grep(!exists $val->{$_}, @keys); + @miss = map "$_".($optnl{$_} ? ' [optional]' : ''), @miss; + my $miss = @miss ? "${fsep}missing fields: ".(join ', ', @miss)."." : ''; + @keys = ( grep(exists $val->{$_}, @keys), sort @ekeys ); # grep: just in case + my %ekeys = map(($_,''), @keys); + @ekeys{@ekeys} = ('?') x @ekeys; + + return $pre_mult . (join $fsep, + map "$ekeys{$_}".sprintf('%-14s',$_)."$val_sep$pre$val->{$_}$post", @keys) . $miss if $bin; + $pre_mult . (join $fsep, map "$ekeys{$_}".sprintf('%-14s',$_)."$val_sep" + . ( $_ =~ /^_(?!encoding)/ ? '__binary_DATA__ [len='.length($val->{$_}).']' + : "$pre$val->{$_}$post" ), @keys) . $miss; +} + +sub __f_long_name ($$) { + my ($self,$fr) = (shift, shift); + (my $short = $fr) =~ s/^(\w{4})\d{2,}/$1/; + $long_names{$short} || '???'; +} + +sub __frames_as_printable { + my ($self,$fr_sep,$fn_sep) = (shift, shift, shift); + my $h = $self->get_frame_ids(); + join $fr_sep, map sprintf('%-40s', + $self->get_frame_descr($_) + . " (" . $self->__f_long_name($_) . ")") + . $fn_sep . $self->__frame_as_printable($_,@_), sort keys %$h; +} + + +=pod + +=item get_frame_option() + + $options = get_frame_option($ID); + + Option is a hash reference, the hash contains all possible options. + The value for each option is 0 or 1. + + groupid -- not supported yet + encryption -- not supported yet + compression -- Compresses frame before writing tag; + compression/uncompression is done automatically + read_only -- Ignored by this library, should be obeyed by application + file_preserv -- Ignored by this library, should be obeyed by application + tag_preserv -- Ignored by this library, should be obeyed by application + +=cut + +sub get_frame_option { + my ($self, $fname)=@_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + return undef unless exists $self->{frames}->{$fname}; + return $self->{frames}->{$fname}->{flags}; +} + +=pod + +=item set_frame_option() + + $options = set_frame_option($ID, $option, $value); + + Set $option to $value (0 or 1). If successfull the new set of + options is returned, undef otherwise. + + groupid -- not supported yet + encryption -- not supported yet + compression -- Compresses frame before writing tag; + compression/uncompression is done automatically + read_only -- Ignored by this library, should be obeyed by application + file_preserv -- Ignored by this library, should be obeyed by application + tag_preserv -- Ignored by this library, should be obeyed by application + + +=cut + +sub set_frame_option { + my ($self, $fname,$option,$value)=@_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + return undef unless exists $self->{frames}->{$fname}; + if (exists $self->{frames}->{$fname}->{flags}->{$option}) { + $self->{frames}->{$fname}->{flags}->{$option}=$value?1:0; + } else { + warn "Unknown option $option\n"; + return undef; + } + return $self->{frames}->{$fname}->{flags}; +} + +sub sort_with_apic { + my ($a_APIC, $b_APIC) = map scalar(/^APIC/), $a, $b; + $a_APIC cmp $b_APIC or $a cmp $b; +} + +# build_tag() +# create a string with the complete tag data +sub build_tag { + my ($self, $ignore_error) = @_; + my $tag_data; + + # in which order should the frames be sorted? + # with a simple sort the order of frames of one type is the order of adding them + my @frames = sort sort_with_apic keys %{$self->{frames}}; + + for my $frameid (@frames) { + my $frame = $self->{frames}->{$frameid}; + + if ($frame->{major} < 3) { + #try to convert to ID3v2.3 or + warn "Can't convert $frameid to ID3v2.3\n"; + next if ($ignore_error); + return undef; + } + my $data = $frame->{data}; + my %flags = (); + #compress data if this is wanted + if ($frame->{flags}->{compression} || $self->{flags}->{compress_all}) { + $flags{compression} = 1; + $data = pack("N", length($data)) . compress $data unless $frame->{flags}->{unchanged}; + } + + #encrypt data if this is wanted + if ($frame->{flags}->{encryption} || $self->{flags}->{encrypt_all}) { + if ($frame->{flags}->{unchanged}) { + $flags{encryption} = 1; + } else { + # ... not supported yet + return undef unless $ignore_error; + warn "Encryption not supported yet\n"; + } + } + + # set groupid + if ($frame->{flags}->{group_id}) { + return undef unless $ignore_error; + warn "Group ids are not supported in writing\n"; + } + + # unsync + my $extra = 0; + if ( ($self->get_config('id3v23_unsync'))->[0] + and ($self->{version} == 3 + and ($self->get_config('id3v23_unsync_size_w'))->[0] + or $self->{version} >= 4) ) { + $extra++ while $data =~ /\xFF(?=[\x00\xE0-\xFF])/g; + } + + #prepare header + my $header = substr($frameid,0,4) . pack("Nn", $extra + length ($data), build_flags(%flags)); + + $tag_data .= $header . $data; + } + return $tag_data; +} + +# insert_space() copies a mp3-file and can insert one or several areas +# of free space for a tag. These areas are defined as +# ($pos, $old_size, $new_size) +# $pos says at which position of the mp3-file the space should be inserted +# new_size gives the size of the space to insert and old_size can be used +# to skip this size in the mp3-file (e.g if +sub insert_space { + my ($self, $insert) = @_; + my $mp3obj = $self->{mp3}; + # !! use a specific tmp-dir here + my $tempfile = dirname($mp3obj->{filename}) . "/TMPxx"; + my $count = 0; + while (-e $tempfile . $count . ".tmp") { + if ($count++ > 999) { + warn "Problems with tempfile\n"; + return undef; + } + } + $tempfile .= $count . ".tmp"; + unless (open (NEW, ">$tempfile")) { + warn "Can't open '$tempfile' to insert tag\n"; + return -1; + } + my ($buf, $pos_old); + binmode NEW; + $pos_old=0; + $mp3obj->seek(0,0); + local $\ = ''; + + foreach my $ins (@$insert) { + if ($pos_old < $ins->[0]) { + $pos_old += $ins->[0]; + while ($mp3obj->read(\$buf,$ins->[0]<16384?$ins->[0]:16384)) { + print NEW $buf; + $ins->[0] = $ins->[0]<16384?0:$ins->[0]-16384; + } + } + for (my $i = 0; $i<$ins->[2]; $i++) { + print NEW chr(0); + } + if ($ins->[1]) { + $pos_old += $ins->[1]; + $mp3obj->seek($pos_old,0); + } + } + + while ($mp3obj->read(\$buf,16384)) { + print NEW $buf; + } + close NEW; + $mp3obj->close; + + # rename tmp-file to orig file + unless (( rename $tempfile, $mp3obj->{filename})|| + (system("mv",$tempfile,$mp3obj->{filename})==0)) { + unlink($tempfile); + warn "Couldn't rename temporary file $tempfile to $mp3obj->{filename}\n"; + return -1; + } + return 0; +} + +=pod + +=item get_frames() + + ($name, @info) = get_frames($ID); + ($name, @info) = get_frames($ID, 'raw'); + +Same as get_frame() with different order of the returned values. +$name and elements of the array @info have the same semantic as for +get_frame(); each frame with id $ID produces one elements of array @info. + +=cut + +sub get_frames { + my ($self, $fname, $raw) = @_; + my ($info, $name, @rest) = $self->get_frame($fname, $raw) or return; + return ($name, $info, @rest); +} + + +=item as_bin() + + $tag2 = $id3v2->as_bin($ignore_error, $update_file, $raw_ok); + +Returns the the current content of the ID3v2 tag as a string good to +write to a file; it contains all the necessary footers and headers. + +If $ignore_error is TRUE, the frames the module does not know how to +write are skipped; otherwise it is an error to have such a frame. +Returns undef on error. + +If the optional argument $update_file is TRUE, an additional action is +performed: if the audio file does not contain an ID3v2 tag, or the tag +in the file is smaller than the built ID3v2 tag, the necessary +0-padding is inserted before the audio content of the file so that it +is able to accommodate the build tag (and the C<tagsize> field of +$id3v2 is updated correspondingly); in any case the header length of +$tag2 is set to reflect the space in the beginning of the audio file. + +Unless $update_file has C<'padding'> as a substring, the actual length of +the string $tag2 is not modified, so if it is smaller than the reserved +space in the file, one needs to add some 0 padding at the end. Note that +if the size of reserved space can shrink (as with C<id3v2_shrink> configuration +option), then without this option it would be hard to calculate necessary +padding by hand. + +If $raw_ok option is given, but not $update_file, the original contents +is returned for unmodified tags. + +=item as_bin_raw() + + $tag2 = $id3v2->as_bin_raw($ignore_error, $update_file); + +same as as_bin() with $raw_ok flag. + +=item write_tag() + + $id3v2->write_tag($ignore_error); + +Saves all frames to the file. It tries to update the file in place, +when the space of the old tag is big enough for the new tag. +Otherwise it creates a temp file with a new tag (i.e. copies the whole +mp3 file) and renames/moves it to the original file name. + +An extended header with CRC checksum is not supported yet. + +Encryption of frames and group ids are not supported. If $ignore_error +is set, these options are ignored and the frames are saved without these options. +If $ignore_error is not set and a tag with an unsupported option should be save, the +tag is not written and a 0 is returned. + +If a tag with an encrypted frame is read, and the frame is not changed +it can be saved encrypted again. + +ID3v2.2 tags are converted automatically to ID3v2.3 tags during +writing. If a frame cannot be converted automatically (PIC; CMR), +writing aborts and returns a 0. If $ignore_error is true, only not +convertable frames are ignored and not written, but the rest of the +tag is saved as ID3v2.3. + +At the moment the tag is automatically unsynchronized. + +If the tag is written successfully, 1 is returned. + +=cut + +sub as_bin_raw ($;$$) { + my ($self, $ignore_error, $update_file) = @_; + $self->as_bin($ignore_error, $update_file, 1); +} + +sub as_bin ($;$$$) { + my ($self, $ignore_error, $update_file, $raw_ok) = @_; + + return $self->{raw_data} + if $raw_ok and $self->{raw_data} and not $self->{modified} and not $update_file; + + die "Writing of ID3v2.4 is not fully supported (prohibited now via `write_v24').\n" + if $self->{major} == 4 and not $self->get_config1('write_v24'); + if ($self->{major} > 4) { + warn "Only writing of ID3v2.3 (and some tags of v2.4) is supported. Cannot convert ID3v". + $self->{version}." to ID3v2.3 yet.\n"; + return undef; + } + + # which order should tags have? + + $self->get_frame_ids; + my $tag_data = $self->build_tag($ignore_error); + return unless defined $tag_data; + + # printing this will ruin flags if they are \x80 or above. + die "panic: prepared raw tag contains wide characters" + if $tag_data =~ /[^\x00-\xFF]/; + # perhaps search for first mp3 data frame to check if tag size is not + # too big and will override the mp3 data + + #ext header are not supported yet + my $flags = chr(0); + $flags = chr(128) if ($self->get_config('id3v23_unsync'))->[0] + and $tag_data =~ s/\xFF(?=[\x00\xE0-\xFF])/\xFF\x00/g; # sync flag + $tag_data .= "\0" # Terminated by 0xFF? + if length $tag_data and chr(0xFF) eq substr $tag_data, -1, 1; + my $n_tsize = length $tag_data; + + my $header = 'ID3' . chr(3) . chr(0); + + if ($update_file) { + my $o_tsize = $self->{buggy_padding_size} + $self->{tagsize}; + my $add_padding = 0; + if ( $o_tsize < $n_tsize + or ($self->get_config('id3v2_shrink'))->[0] ) { + # if creating new tag / increasing size add at least 128b padding + # add additional bytes to make new filesize multiple of 512b + my $mp3obj = $self->{mp3}; + my $filesize = (stat($mp3obj->{filename}))[7]; + my $extra = ($self->get_config('id3v2_minpadding'))->[0]; + my $n_filesize = ($filesize + $n_tsize - $o_tsize + $extra); + my $round = ($self->get_config('id3v2_sizemult'))->[0]; + $n_filesize = (($n_filesize + $round - 1) & ~($round - 1)); + my $n_padding = $n_filesize - $filesize - ($n_tsize - $o_tsize); + $n_tsize += $n_padding; + if ($o_tsize != $n_tsize) { + my @insert = [0, $o_tsize+10, $n_tsize + 10]; + return undef unless insert_space($self, \@insert) == 0; + } else { # Slot is not filled by 0; fill it manually + $add_padding = $n_padding - $self->{buggy_padding_size}; + } + $self->{tagsize} = $n_tsize; + } else { # Include current "padding" into n_tsize + $add_padding = $self->{tagsize} - $n_tsize; + $n_tsize = $self->{tagsize} = $o_tsize; + } + $add_padding = 0 if $add_padding < 0; + $tag_data .= "\0" x $add_padding if $update_file =~ /padding/; + } + + #convert size to header format specific size + my $size = unpack('B32', pack ('N', $n_tsize)); + substr ($size, -$_, 0) = '0' for (qw/28 21 14 7/); + $size= pack('B32', substr ($size, -32)); + + return "$header$flags$size$tag_data"; +} + +sub write_tag { + my ($self,$ignore_error) = @_; + $self->fix_frames_encoding() + if $self->get_config1('id3v2_fix_encoding_on_write'); + + $self->get_frame_ids; # Ensure all the reading is done... + # Need to do early, otherwise file size for calculation of "best" padding + # may not take into account the added ID3v1 tag + my $mp3obj = $self->{mp3}; + $mp3obj->close; + unless ($mp3obj->open("write")) { + warn "Couldn't open file `",$mp3obj->filename(),"' to write tag!"; + return undef; + } + + my $tag = $self->as_bin($ignore_error, 'update_file, with_padding'); + return 0 unless defined $tag; + + $mp3obj->close; + unless ($mp3obj->open("write")) { # insert_space() could've closed the file + warn "Couldn't open file `",$mp3obj->filename(),"' to write tag!"; + return undef; + } + + # actually write the tag + $mp3obj->seek(0,0); + $mp3obj->write($tag); + $mp3obj->close; + return 1; +} + +=pod + +=item remove_tag() + + $id3v2->remove_tag(); + +Removes the whole tag from the file by copying the whole +mp3-file to a temp-file and renaming/moving that to the +original filename. + +Do not use remove_tag() if you only want to change a header, +as otherwise the file is copied unnecessarily. Use write_tag() +directly, which will override an old tag. + +=cut + +sub remove_tag { + my $self = shift; + my $mp3obj = $self->{mp3}; + my $tempfile = dirname($mp3obj->{filename}) . "/TMPxx"; + my $count = 0; + local $\ = ''; + while (-e $tempfile . $count . ".tmp") { + if ($count++ > 999) { + warn "Problems with tempfile\n"; + return undef; + } + } + $tempfile .= $count . ".tmp"; + if (open (NEW, ">$tempfile")) { + my $buf; + binmode NEW; + $mp3obj->seek($self->{tagsize}+10,0); + while ($mp3obj->read(\$buf,16384)) { + print NEW $buf; + } + close NEW; + $mp3obj->close; + unless (( rename $tempfile, $mp3obj->{filename})|| + (system("mv",$tempfile,$mp3obj->{filename})==0)) { + warn "Couldn't rename temporary file $tempfile\n"; + } + } else { + warn "Couldn't write temp file\n"; + return undef; + } + return 1; +} + +=pod + +=item add_frame() + + $fn = $id3v2->add_frame($fname, @data); + +Add a new frame, identified by the short name $fname. The number of +elements of array @data should be as described in the ID3v2.3 +standard. (See also L<MP3::Tag::ID3v2_Data>.) There are two +exceptions: if @data is empty, it is filled with necessary number of +C<"">); if one of required elements is C<encoding>, it may be omitted +or be C<undef>, meaning the arguments are in "Plain Perl (=ISOLatin-1 +or Unicode) encoding". + +It returns the the short name $fn (which can differ from +$fname, when an $fname frame already exists). If no +other frame of this kind is allowed, an empty string is +returned. Otherwise the name of the newly created frame +is returned (which can have a 01 or 02 or ... appended). + +You have to call write_tag() to save the changes to the file. + +Examples (with C<$id3v2-E<gt>> omitted): + + $f = add_frame('TIT2', 0, 'Abba'); # $f='TIT2' + $f = add_frame('TIT2', 'Abba'); # $f='TIT201', encoding=0 implicit + + $f = add_frame('COMM', 'ENG', 'Short text', 'This is a comment'); + + $f = add_frame('COMM'); # creates an empty frame + + $f = add_frame('COMM', 'ENG'); # ! wrong ! $f=undef, becaues number + # of arguments is wrong + + $f = add_frame('RBUF', $n1, $n2, $n3); + $f = add_frame('RBUF', $n1, $n2); # last field of RBUF is optional + +If a frame has optional fields I<and> C<encoding> (only C<COMR> frame +as of ID3v2.4), there may be an ambiguity which fields are omitted. +It is resolved this way: the C<encoding> field can be omitted only if +all other optional frames are omitted too (set it to C<undef> +instead). + +=item add_frame_split() + +The same as add_frame(), but if the number of arguments is +unsufficient, would split() the last argument on C<;> to obtain the +needed number of arguments. Should be avoided unless it is known that +the fields do not contain C<;> (except for C<POPM RBUF RVRB SYTC>, +where splitting may be done non-ambiguously). + + # No ambiguity, since numbers do not contain ";": + $f = add_frame_split('RBUF', "$n1;$n2;$n3"); + +For C<COMR> frame, in case when the fields are C<join()>ed by C<';'>, +C<encoding> field may be present only if all the other fields are +present. + +=cut + +# 0 = latin1 (effectively: unknown) +# 1 = UTF-16 with BOM (we always write UTF-16le to cowtow to M$'s bugs) +# 2 = UTF-16be, no BOM +# 3 = UTF-8 +my @dec_types = qw( iso-8859-1 UTF-16 UTF-16BE utf8 ); +my @enc_types = qw( iso-8859-1 UTF-16LE UTF-16BE utf8 ); +my @tail_rex; + +# Actually, disable this code: it always triggers unsync... +my $use_utf16le = $ENV{MP3TAG_USE_UTF_16LE}; +@enc_types = @dec_types unless $use_utf16le; + +sub _add_frame { + my ($self, $split, $fname, @data) = @_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + my $format = get_format($fname); + return undef unless defined $format; + + #prepare the data + my $args = @$format; my $opt = 0; + + unless (@data) { + @data = map {''} @$format; + } + + my($encoding, $calc_enc, $e, $e_add) = (0,0); # Need to calculate encoding? + # @data may be smaller than @args due to missing encoding, or due + # to optional arguments. Both may be applicable for COMR frames. + if (@data < $args) { + $_->{optional} and $opt++ for @$format; + $e_add++, unshift @data, undef # Encoding skipped + if (@data == $args - 1 - $opt or $split and @data <= $args - 1 - $opt) + and $format->[0]->{name} eq '_encoding'; + if ($opt) { # encoding is present only for COMR, require it + die "Data for `encoding' should be between 0 and 3" + if $format->[0]->{name} eq "_encoding" + and defined $data[0] and not $data[0] =~ /^[0-3]?$/; + } + } + if ($split and @data < $args) { + if ($back_splt{$fname}) { + my $c = $args - @data; + my $last = pop @data; + my $rx = ($tail_rex[$c] ||= qr/((?:;[^;]*){0,$c})\z/); + my($tail) = ($last =~ /$rx/); # Will always match + push @data, substr $last, 0, length($last)-length($tail); + if ($tail =~ s/^;//) { # matched >= 1 times + push @data, split ';', $tail; + } + } else { + my $last = pop @data; + push @data, split /;/, $last, $args - @data; + } + # Allow for explicit specification of encoding + shift @data if @data == $args + 1 and not defined $data[0] + and $format->[0]->{name} eq '_encoding'; # Was auto-put there + } + die "Unexpected number of fields: ".@data.", expect $args, optional=$opt" + unless @data <= $args and @data >= $args - $opt; + if ($format->[0]->{name} eq "_encoding" and not defined $data[0]) { + $calc_enc = 1; + shift @data; + } + + my ($datastring, $have_high) = ""; + if ($calc_enc) { + my @d = @data; + foreach my $fs (@$format) { + $have_high = 1 if $fs->{encoded} and $d[0] and $d[0] =~ /[^\x00-\xff]/; + shift @d unless $fs->{name} eq "_encoding"; + } + } + foreach my $fs (@$format) { + next if $fs->{optional} and not @data; + if ($fs->{name} eq "_encoding") { + if ($calc_enc) { + $encoding = ($have_high ? 1 : 0); # v2.3 only has 0, 1 + } else { + $encoding = shift @data; + } + $datastring .= chr($encoding); + next; + } + my $d = shift @data; + if ($fs->{isnum}) { + ## store data as number + my $num = int($d); + $d=""; + while ($num) { $d=pack("C",$num % 256) . $d; $num = int($num/256);} + if ( exists $fs->{len} and $fs->{len}>0 ) { + $d = substr $d, -$fs->{len}; + $d = ("\x00" x ($fs->{len}-length($d))) . $d if length($d) < $fs->{len}; + } + if ( exists $fs->{mlen} and $fs->{mlen}>0 ) { + $d = ("\x00" x ($fs->{mlen}-length($d))) . $d if length($d) < $fs->{mlen}; + } + } elsif ( exists $fs->{len} and not exists $fs->{func}) { + if ($fs->{len}>0) { + $d = substr $d, 0, $fs->{len}; + $d .= " " x ($fs->{len}-length($d)) if length($d) < $fs->{len}; + } elsif ($fs->{len}==0) { + $d .= chr(0); + } + } elsif (exists $fs->{mlen} and $fs->{mlen}>0) { + $d .= " " x ($fs->{mlen}-length($d)) if length($d) < $fs->{mlen}; + } + if (exists $fs->{re2b}) { + while (my ($pat, $rep) = each %{$fs->{re2b}}) { + $d =~ s/$pat/$rep/gis; + } + } + if (exists $fs->{func_back}) { + $d = $fs->{func_back}->($d); + } elsif (exists $fs->{func}) { + if ($fs->{small_max}) { # Allow the old way (byte) and a number + # No conflict possible: byte is always smaller than ord '0' + $d = pack 'C', $d if $d =~ /^\d+$/; + } + $d = $self->__format_field($fname, $fs->{name}, $d) + } + if ($fs->{encoded}) { + if ($encoding) { + # 0 = latin1 (effectively: unknown) + # 1 = UTF-16 with BOM (we write UTF-16le to cowtow to M$'s bugs) + # 2 = UTF-16be, no BOM + # 3 = UTF-8 + require Encode; + if ($calc_enc or $encode_utf8) { # e_u8==1 by default + $d = Encode::encode($enc_types[$encoding], $d); + } elsif ($encoding < 3) { + # Reencode from UTF-8 + $d = Encode::decode('UTF-8', $d); + $d = Encode::encode($enc_types[$encoding], $d); + } + $d = "\xFF\xFE$d" if $use_utf16le and $encoding == 1; + } elsif (not $self->{fixed_encoding} # Now $encoding == 0... + and $self->get_config1('id3v2_fix_encoding_on_edit') + and $e = $self->botched_encoding() + and do { require Encode; Encode::decode($e, $d) ne $d }) { + # If the current string is interpreted differently + # with botched_encoding, need to unbotch... + $self->fix_frames_encoding(); + } + } + $datastring .= $d; + } + + return add_raw_frame($self, $fname, $datastring); +} + +sub add_frame { + my $self = shift; + _add_frame($self, 0, @_) +} + +sub add_frame_split { + my $self = shift; + _add_frame($self, 1, @_) +} + +sub add_raw_frame ($$$$) { + my($self, $fname, $datastring, $flags) = (shift,shift,shift,shift); + + #add frame to tag + if (exists $self->{frames}->{$fname}) { + my ($c, $ID) = (1, $fname); + $fname .= '01'; + while (exists $self->{frames}->{$fname}) { + $fname++, $c++; + } + ++$self->{extra_frames}->{$ID} + if $c > ($self->{extra_frames}->{$ID} || 0); + } + $self->{frames}->{$fname} = {flags => ($flags || $self->check_flags(0)), + major => $self->{frame_major}, + data => $datastring }; + $self->{modified}++; + return $fname; +} + +=pod + +=item change_frame() + + $id3v2->change_frame($fname, @data); + +Change an existing frame, which is identified by its +short name $fname eg as returned by get_frame_ids(). +@data must be same as in add_frame(). + +If the frame $fname does not exist, undef is returned. + +You have to call write_tag() to save the changes to the file. + +=cut + +sub change_frame { + my ($self, $fname, @data) = @_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + return undef unless exists $self->{frames}->{$fname}; + + $self->remove_frame($fname); + $self->add_frame($fname, @data); + + return 1; +} + +=pod + +=item remove_frame() + + $id3v2->remove_frame($fname); + +Remove an existing frame. $fname is the short name of a frame, +eg as returned by get_frame_ids(). + +You have to call write_tag() to save the changes to the file. + +=cut + +sub remove_frame { + my ($self, $fname) = @_; + $self->get_frame_ids() unless exists $self->{frameIDs}; + return undef unless exists $self->{frames}->{$fname}; + delete $self->{frames}->{$fname}; + $self->{modified}++; + return 1; +} + +=item copy_frames($from, $to, $overwrite, [$keep_flags, $f_ids]) + +Copies specified frames between C<MP3::Tag::ID3v2> objects $from, $to. Unless +$keep_flags, the copied frames have their flags cleared. +If the array reference $f_ids is not specified, all the frames (but C<GRID> +and C<TLEN>) are considered (subject to $overwrite), otherwise $f_ids should +contain short frame ids to consider. Group ID flag is always cleared. + +If $overwrite is C<'delete'>, frames with the same descriptors (as +returned by get_frame_descr()) in $to are deleted first, then all the +specified frames are copied. If $overwrite is FALSE, only frames with +descriptors not present in $to are copied. (If one of these two +conditions is not met, the result may be not conformant to standards.) + +Returns count of copied frames. + +=cut + +sub copy_frames { + my ($from, $to, $overwrite, $keep_flags, $f_ids) = @_; +# return 0 unless $from->{ID3v2}; # No need to create it... + my($cp, $expl) = (0, $f_ids); + $f_ids ||= [keys %{$from->get_frame_ids}]; + for my $fn (@$f_ids) { + next if not $expl and $fn =~ /^(GRID|TLEN)/; + if (($overwrite || 0) eq 'delete') { + $to->frame_select_by_descr($from->get_frame_descr($fn), undef); # delete + } elsif (not $overwrite) { + next if $to->frame_have($from->get_frame_descr($fn)); + } + my $f = $from->{frames}->{$fn}; + $fn =~ s/^(\w{4})\d+$/$1/; + my $d = $f->{data}; + my %fl = %{$f->{flags}}; + (substr $d, 0, 1) = '' if delete $fl{groupid}; + $to->add_raw_frame($fn, $d, $keep_flags ? \%fl : undef); + $cp++; + } + return $cp +} + +=item is_modified() + + $id3v2->is_modified; + +Returns true if the tag was modified after it was created. + +=cut + +sub is_modified { + shift->{modified} +} + +=pod + +=item supported_frames() + + $frames = $id3v2->supported_frames(); + +Returns a hash reference with all supported frames. The keys of the +hash are the short names of the supported frames, the +according values are the long (english) names of the frames. + +=cut + +sub supported_frames { + my $self = shift; + + my (%tags, $fname, $lname); + while ( ($fname, $lname) = each %long_names) { + $tags{$fname} = $lname if get_format($fname, "quiet"); + } + + return \%tags; +} + +=pod + +=item what_data() + + ($data, $res_inp, $data_map) = $id3v2->what_data($fname); + +Returns an array reference with the needed data fields for a +given frame. +At this moment only the internal field names are returned, +without any additional information about the data format of +this field. Names beginning with an underscore (normally '_data') +can contain binary data. (The C<_encoding> field is skipped in this list, +since it is usually auto-deduced by this module.) + +$resp_inp is a reference to a hash (keyed by the field name) describing +restrictions for the content of the data field. +If the entry is undef, no restriction exists. Otherwise it is a hash. +The keys of the hash are the allowed input, the correspodending value +is the value which is actually stored in this field. If the value +is undef then the key itself is valid for saving. +If the hash contains an entry with "_FREE", the hash contains +only suggestions for the input, but other input is also allowed. + +$data_map contains values of $resp_inp in the order of fields of a frame +(including C<_encoding>). + +Example for picture types of the APIC frame: + + {"Other" => "\x00", + "32x32 pixels 'file icon' (PNG only)" => "\x01", + "Other file icon" => "\x02", + ...} + +=cut + +sub what_data { + my ($self, $fname) = @_; + $fname = substr $fname, 0, 4; # delete 01 etc. at end + return if length($fname)==3; #id3v2.2 tags are read-only and should never be written + my $reswanted = wantarray; + my $format = get_format($fname, "quiet"); + return unless defined $format; + my (@data, %res, @datares); + + foreach (@$format) { + next unless exists $_->{name}; + push @data, $_->{name} unless $_->{name} eq "_encoding"; + next unless $reswanted; + my $key = $fname . $_->{name}; + $res{$_->{name}} = $field_map{$key} if exists $field_map{$key}; + push @datares, $field_map{$key}; + } + + return(\@data, \%res, \@datares) if $reswanted; + return \@data; +} + +sub __format_field { + my ($self, $fname, $nfield, $v) = @_; + # $v =~ s/^(\d+)$/chr $1/e if $is_small_int{"$fname$nfield"}; # Already done by caller + + my $m = $field_map_back{my $t = "$fname$nfield"} or return $v; # packed ==> Human + return $v if exists $m->{$v}; # Already of a correct form + + my $m1 = $field_map{$t} or die; # Human ==> packed + return $m1->{$v} if exists $m1->{$v}; # translate + return $v if $m->{_FREE}; # Free-form allowed + + die "Unsupported value `$v' for field `$nfield' of frame `$fname'"; +} + +=item title( [@new_title] ) + +Returns the title composed of the tags configured via C<MP3::Tag-E<gt>config('v2title')> +call (with default 'Title/Songname/Content description' (TIT2)) from the tag. +(For backward compatibility may be called by deprecated name song() as well.) + +Sets TIT2 frame if given the optional arguments @new_title. If this is an +empty string, the frame is removed. + +=cut + +*song = \&title; + +sub v2title_order { + my $self = shift; + @{ $self->get_config('v2title') }; +} + +sub title { + my $self = shift; + if (@_) { + $self->remove_frame('TIT2'); # NOP if it is not there + return if @_ == 1 and $_[0] eq ''; + return $self->add_frame('TIT2', @_); + } + my @parts = grep defined && length, + map scalar $self->get_frame($_), $self->v2title_order; + return unless @parts; + my $last = pop @parts; + my $part; + for $part (@parts) { + $part =~ s(\0)(///)g; # Multiple strings + $part .= ',' unless $part =~ /[.,;:\n\t]\s*$/; + $part .= ' ' unless $part =~ /\s$/; + } + return join '', @parts, $last; +} + +=item _comment([$language]) + +Returns the file comment (COMM with an empty 'Description') from the tag, or +"Subtitle/Description refinement" (TIT3) frame (unless it is considered a part +of the title). + +=cut + +sub _comment { + my $self = shift; + my $language; + $language = lc shift if @_; + my @info = get_frames($self, "COMM"); + shift @info; + for my $comment (@info) { + next unless defined $comment; # Removed frames + next unless exists $comment->{Description} and not length $comment->{Description}; + next if defined $language and (not exists $comment->{Language} + or lc $comment->{Language} ne $language); + return $comment->{Text}; + } + return if grep $_ eq 'TIT3', $self->v2title_order; + return scalar $self->get_frame("TIT3"); +} + +=item comment() + + $val = $id3v2->comment(); + $newframe = $id3v2->comment('Just a comment for freddy', 'personal', 'eng'); + +Returns the file comment (COMM frame with the 'Description' field in +C<default_descr_c> configuration variable, defalting to C<''>) from +the tag, or "Subtitle/Description refinement" (TIT3) frame (unless it +is considered a part of the title). + +If optional arguments ($comment, $short, $language) are present, sets +the comment frame. If $language is omited, uses the +C<default_language> configuration variable (default is C<XXX>). If not +C<XXX>, this should be lowercase 3-letter abbreviation according to +ISO-639-2). + +If $short is not defined, uses the C<default_descr_c> configuration +variable. If $comment is an empty string, the frame is removed. + +=cut + +sub comment { + my $self = shift; + my ($comment, $short, $language) = @_ or return $self->_comment(); + my @info = get_frames($self, "COMM"); + my $desc = ($self->get_config('default_descr_c'))->[0]; + shift @info; + my $c = -1; + for my $comment (@info) { + ++$c; + next unless defined $comment; # Removed frames + next unless exists $comment->{Description} + and $comment->{Description} eq $desc; + next if defined $language and (not exists $comment->{Language} + or lc $comment->{Language} ne lc $language); + $self->remove_frame($c ? sprintf 'COMM%02d', $c : 'COMM'); + # $c--; # Not needed if only one frame is removed + last; + } + return if @_ == 1 and $_[0] eq ''; + $language = ($self->get_config('default_language'))->[0] + unless defined $language; + $short = $desc unless defined $short; + $self->add_frame('COMM', $language, $short, $comment); +} + +=item frame_select($fname, $descrs, $languages [, $newval1, ...]) + +Used to get/set/delete frames which may be not necessarily unique in a tag. + + # Select short-description='', prefere language 'eng', then 'rus', then + # the third COMM frame, then any (in this case, the first or the second) + # COMM frame + $val = $id3v2->frame_select('COMM', '', ['eng', 'rus', '#2', '']); # Read + $new = $id3v2->frame_select('COMM', '', ['eng', 'rus', '#2'], # Write + 'Comment with empty "Description" and "eng"'); + $new = $id3v2->frame_select('COMM', '', ['eng', 'rus', '#2'], # Delete + undef); + +Returns the contents of the first frame named $fname with a +'Description' field in the specified array reference $descrs and the +language in the list of specified languages $languages; empty return +otherwise. If the frame is a "simple frame", the frame is returned as +a string, otherwise as a hash reference; a "simple frame" should +consist of one of Text/URL/_Data fields, with possible addition of +Language and Description fields (if the corresponding arguments were +defined). + +The lists $descrs and $languages of one element can be flattened to +become this element (as with C<''> above). If the lists are not +defined, no restriction is applied; to get the same effect with +defined arguments, use $languages of C<''>, and/or $descrs a hash +reference. Language of the form C<'#NUMBER'> selects the NUMBER's +(0-based) frame with frame name $fname. + +If optional arguments C<$newval1...> are given, B<ALL> the found frames are +removed; if only one such argument C<undef> is given, this is the only action. +Otherwise, a new frame is created afterwards (the first +elements of $descrs and $languages are used as the short description +and the language, defaulting to C<''> and the C<default_language> +configuration variable (which, in turn, defaults to C<XXX>; if not C<XXX>, +this should be lowercase 3-letter abbreviation according to ISO-639-2). +If new frame is created, the frame's name is returned; otherwise the count of +removed frames is returned. + +As a generalization, APIC frames are handled too, using C<Picture +Type> instead of C<Language>, and auto-calculating C<MIME type> for +(currently) TIFF/JPEG/GIF/PNG/BMP and octet-stream. Only frames with +C<MIME type> coinciding with the auto-calculated value are considered +as "simple frames". One can use both the 1-byte format for C<Picture +Type>, and the long names used in the ID3v2 documentation; the default +value is C<'Cover (front)'>. + + # Choose APIC with empty description, picture_type='Leaflet page' + my $data = $id3v2->frame_select('APIC', '', 'Leaflet page') + or die "no expected APIC frame found"; + my $format = ( ref $data ? $data->{'MIME type'} + : $id3v2->_Data_to_MIME($data) ); + # I know what to do with application/pdf only (sp?) and 'image/gif' + die "Do not know what to do with this APIC format: `$format'" + unless $format eq 'application/pdf' or $format eq 'image/gif'; + $data = $data->{_Data} if ref $data; # handle non-simple frame + + # Set APIC frame with empty description (front cover if no other present) + # from content of file.gif + my $data = do { open my $f, '<', 'file.gif' and binmode $f or die; + undef $/; <$f>}; + my $new_frame = $id3v2->frame_select('APIC', '', undef, $data); + +Frames with multiple "content" fields may be set by providing multiple +values to set. Alternatively, one can also C<join()> the values with +C<';'> if the splitting is not ambiguous, e.g., for C<POPM RBUF RVRB +SYTC>. (For frames C<GEOD> and C<COMR>, which have a C<Description> +field, it should be specified among these values.) + + $id3v2->frame_select("RBUF", undef, undef, $n1, $n2, $n3); + $id3v2->frame_select("RBUF", undef, undef, "$n1;$n2;$n3"); + +(By the way: consider using the method select_id3v2_frame() on the +"parent" MP3::Tag object instead [see L<MP3::Tag/select_id3v2_frame>], +or L<frame_select_by_descr()>.) + +=item _Data_to_MIME + +Internal method to extract MIME type from a string the image file +content. Returns C<application/octet-stream> for unrecognized data +(unless extra TRUE argument is given). + + $format = $id3v2->_Data_to_MIME($data); + +Currently, only the first 4 bytes of the string are inspected. + +=cut + +sub __to_lang($$) {my $l = shift; return $l if shift or $l eq 'XXX'; lc $l} + +my %as_lang = ('APIC', ['Picture Type', chr 3, 'small_int']); # "Cover (front)" +my %MT = ("\xff\xd8\xff\xe0" => 'image/jpeg', "MM\0*" => 'image/tiff', + "II*\0" => 'image/tiff', "\x89PNG", + qw(image/png GIF8 image/gif BM image/bmp)); + +sub _Data_to_MIME ($$;$) { + my($self, $data, $force) = (shift, shift, shift); # Fname, field name remain + my $res = $MT{substr $data, 0, 4} || $MT{substr $data, 0, 2}; + return $res if $res; + return 'audio/mpeg' if $data =~ /^\xff[\xe0-\xff]/; # 11 bits are 1 + return 'application/octet-stream' unless $force; + return; +} + +sub _frame_select { # if $extract_content false, return all found + # "Quadratic" in number of comment frames and select-short/lang specifiers + my ($self, $extract_content, $fname) = (shift, shift, shift); + my ($descr, $languages) = (shift, shift); +# or ($fname eq 'COMM' and return $self->_comment()); # ??? + my $any_descr; + if (ref $descr eq 'HASH') { # Special case + $any_descr = 1; + undef $descr; + } elsif (defined $descr and not ref $descr) { + $descr = [$descr]; + } + my $lang_special = $as_lang{$fname}; + my $lang_field = ($lang_special ? $lang_special->[0] : 'Language'); + my $languages_mangled; + + if (defined $languages) { + $languages = [$languages] unless ref $languages; + $languages = [@$languages]; # Make a copy: we edit the entries... + if ($lang_special) { + my $m = $field_map{"$fname$lang_field"}; + if ($m) { # Below we assume that mapped values are not ''... + # Need to duplicate the logic in add_frame() here, since + # we need a normalized form to compare frames-to-select with... + if ($lang_special->[2]) { # small_int + s/^(\d+)$/chr $1/e for @$languages; + } + @$languages_mangled = map( (exists $m->{$_} ? $m->{$_} : $_), @$languages); + my $m1 = $field_map_back{"$fname$lang_field"} or die; + my $loose = $m->{_FREE}; + @$languages = map( (exists $m1->{$_} ? $m1->{$_} : $_), @$languages_mangled); + $_ eq '' or exists $m1->{$_} or $loose or not /\D/ + or die "Unknown value `$_' for field `$lang_field' of frame $fname" + for @$languages_mangled; + } + } else { + @$languages = map __to_lang($_, 0), @$languages; + } + } + my @found_frames = get_frames($self, $fname); + shift @found_frames; + my(@by_lang) = (0..$#found_frames); + # Do it the slow way... + if (defined $languages) { + @by_lang = (); + my %seen; + for my $l (@$languages) { + if ($l =~ /^#(\d+)$/) { + push @by_lang, $1 if not $seen{$1}++ and $1 < @found_frames; + } elsif (length $l > 3 and not $lang_special) { + die "Language `$l' should not be more than 3-chars long"; + } else { + for my $c (0..$#found_frames) { + my $f = $found_frames[$c] or next; # XXXX Needed? + push @by_lang, $c + if ($l eq '' + or ref $f and defined $f->{$lang_field} + and $l eq __to_lang $f->{$lang_field}, $lang_special) + and not $seen{$c}++; + } + } + } + } + my @select; + for my $c (@by_lang) { + my $f = $found_frames[$c]; + my $cc = [$c, $f]; + push(@select, $cc), next unless defined $descr; + push @select, $cc + if defined $f and ref $f and defined $f->{Description} + and grep $_ eq $f->{Description}, @$descr; + } + return @select unless $extract_content; + unless (@_) { # Read-only access + return unless @select; + my $res = $select[0][1]; # Only defined frames here... + return $res unless ref $res; # TLEN + my $ic = my $c = keys %$res; + $c-- if exists $res->{Description} and (defined $descr or $any_descr); + $c-- if exists $res->{$lang_field} and defined $languages; + $c-- if exists $res->{encoding}; + $c-- if $c == 2 and $ic == 5 and exists $res->{'MIME type'} + and exists $res->{_Data} + and $res->{'MIME type'} eq $self->_Data_to_MIME($res->{_Data}); + if ($c <= 1) { + return $res->{Text} if exists $res->{Text}; + return $res->{URL} if exists $res->{URL}; + return $res->{_Data} if exists $res->{_Data}; + } + return $res; + } + # Write or delete + for my $f (reverse @select) { # Removal may break the numeration??? + my ($c, $frame) = @$f; + $self->remove_frame($c ? sprintf('%s%02d', $fname, $c) : $fname); + } + return scalar @select unless @_ > 1 or defined $_[0]; # Delete + if (defined $languages) { + $languages = $languages_mangled if defined $languages_mangled; + } elsif ($lang_special) { + $languages = [$lang_special->[1]]; + } else { + $languages = [@{$self->get_config('default_language')}]; # Copy to modify + } + my $format = get_format($fname); + my $have_lang = grep $_->{name} eq $lang_field, @$format; + $#$languages = $have_lang - 1; # Truncate + unshift @$languages, $self->_Data_to_MIME($_[0]) + if $lang_special and @_ == 1; # "MIME type" field + $descr = [''] unless defined $descr; + my $have_descr = grep $_->{name} eq 'Description', @$format; + $have_descr = 0 if $embedded_Descr{$fname}; # Must be explicitly provided + $#$descr = $have_descr - 1; # Truncate + $self->add_frame_split($fname, @$languages, @$descr, @_) or die; +} + +sub frame_select { + my $self = shift; + $self->_frame_select(1, @_); +} + +=item frame_list() + +Same as frame_select(), but returns the list of found frames, each an +array reference C<[$N, $f]> with $N the 0-based ordinal (among frames +with the given short name), and $f the contents of a frame. + +=item frame_have() + +Same as frame_select(), but returns the count of found frames. + +=item frame_select_by_descr() + +=item frame_have_by_descr() + +=item frame_list_by_descr() + + $c = $id3v2->frame_select_by_descr("COMM(fre,fra,eng,#0)[]"); + $t = $id3v2->frame_select_by_descr("TIT2"); + $id3v2->frame_select_by_descr("TIT2", "MyT"); # Set/Change + $id3v2->frame_select_by_descr("RBUF", $n1, $n2, $n3); # Set/Change + $id3v2->frame_select_by_descr("RBUF", "$n1;$n2;$n3"); # Set/Change + $id3v2->frame_select_by_descr("TIT2", undef); # Remove + +Same as frame_select(), frame_have(), frame_list(), but take one string +argument instead of $fname, $descrs, $languages. The argument should +be of the form + + NAME(langs)[descr] + +Both C<(langs)> and C<[descr]> parts may be omitted; I<langs> should +contain comma-separated list of needed languages; no protection by +backslashes is needed in I<descr>. frame_select_by_descr() will +return a hash if C<(lang)> is omited, but the frame has a language +field; likewise for C<[descr]>; see below for alternatives. + +Remember that when frame_select_by_descr() is used for modification, +B<ALL> found frames are deleted before a new one is added. + +(By the way: consider using the method select_id3v2_frame_by_descr() on the +"parent" MP3::Tag object instead; see L<MP3::Tag/select_id3v2_frame_by_descr>.) + +=item frame_select_by_descr_simple() + +Same as frame_select_by_descr(), but if no language is given, will not +consider the frame as "complicated" frame even if it contains a +language field. + +=item frame_select_by_descr_simpler() + +Same as frame_select_by_descr_simple(), but if no C<Description> is +given, will not consider the frame as "complicated" frame even if it +contains a C<Description> field. + +=cut + +sub frame_have { + my $self = shift; + scalar $self->_frame_select(0, @_); +} + +sub frames_list { + my $self = shift; + $self->_frame_select(0, @_); +} + +sub _frame_select_by_descr { + my ($self, $what, $d) = (shift, shift, shift); + my($l, $descr) = (''); + if ( $d =~ s/^(\w{4})(?:\(([^()]*(?:\([^()]+\)[^()]*)*)\))?(?:\[(.*)\])?$/$1/ ) { + $l = defined $2 ? [split /,/, $2, -1] : ($what > 1 && !@_ ? '' : undef); + # Use special case in _frame_select: + $descr = defined $3 ? $3 : ($what > 2 && !@_ ? {} : undef); + # $descr =~ s/\\([\\\[\]])/$1/g if defined $descr; + } + return $self->_frame_select($what, $d, $descr, $l, @_); +} + +sub frame_have_by_descr { + my $self = shift; + scalar $self->_frame_select_by_descr(0, @_); +} + +sub frame_list_by_descr { + my $self = shift; + $self->_frame_select_by_descr(0, @_); +} + +sub frame_select_by_descr { + my $self = shift; + $self->_frame_select_by_descr(1, @_); +} + +sub frame_select_by_descr_simple { + my $self = shift; + $self->_frame_select_by_descr(2, @_); # 2 ==> prefer $languages eq ''... +} + +sub frame_select_by_descr_simpler { + my $self = shift; + $self->_frame_select_by_descr(3, @_); # 2 ==> prefer $languages eq ''... +} + +=item year( [@new_year] ) + +Returns the year (TYER/TDRC) from the tag. + +Sets TYER and TDRC frames if given the optional arguments @new_year. If this +is an empty string, the frame is removed. + +The format is similar to timestamps of IDv2.4.0, but ranges can be separated +by C<-> or C<-->, and non-contiguous dates are separated by C<,> (comma). If +periods need to be specified via duration, then one needs to use the ISO 8601 +C</>-notation (e.g., see + + http://www.mcs.vuw.ac.nz/technical/software/SGML/doc/iso8601/ISO8601.html + +); the C<duration/end_timestamp> is not supported. + +On output, ranges of timestamps are converted to C<-> or C<--> separated +format depending on whether the timestamps are years, or have additional +fields. + +If configuration variable C<year_is_timestamp> is false, the return value +is always the year only (of the first timestamp of a composite timestamp). + +Recall that ID3v2.4.0 timestamp has format yyyy-MM-ddTHH:mm:ss (year, "-", +month, "-", day, "T", hour (out of +24), ":", minutes, ":", seconds), but the precision may be reduced by +removing as many time indicators as wanted. Hence valid timestamps +are +yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and +yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use +the slash character as described in 8601, and for multiple noncontiguous +dates, use multiple strings, if allowed by the frame definition. + +=cut + +sub year { + my $self = shift; + if (@_) { + $self->remove_frame('TYER') if defined $self->get_frame( "TYER"); + $self->remove_frame('TDRC') if defined $self->get_frame( "TDRC"); + return if @_ == 1 and $_[0] eq ''; + my @args = @_; + $args[-1] =~ s/^(\d{4}\b).*/$1/; + $self->add_frame('TYER', @args); # Obsolete + @args = @_; + $args[-1] =~ s/-(-|(?=\d{4}\b))/\//g; # ranges are /-separated + $args[-1] =~ s/,(?=\d{4}\b)/\0/g; # dates are \0-separated + $args[-1] =~ s#([-/T:])(?=\d(\b|T))#${1}0#g; # %02d-format + return $self->add_frame('TDRC', @args); # new; allows YYYY-MM-etc as well + } + my $y; + ($y) = $self->get_frame( "TDRC", 'intact') + or ($y) = $self->get_frame( "TYER") or return; + return substr $y, 0, 4 unless ($self->get_config('year_is_timestamp'))->[0]; + # Convert to human-readable form + $y =~ s/\0/,/g; + my $sep = ($y =~ /-/) ? '--' : '-'; + $y =~ s#/(?=\d)#$sep#g; + return $y; +} + +=pod + +=item track( [$new_track] ) + +Returns the track number (TRCK) from the tag. + +Sets TRCK frame if given the optional arguments @new_track. If this is an +empty string or 0, the frame is removed. + +=cut + +sub track { + my $self = shift; + if (@_) { + $self->remove_frame('TRCK') if defined $self->get_frame("TRCK"); + return if @_ == 1 and not $_[0]; + return $self->add_frame('TRCK', @_); + } + return scalar $self->get_frame("TRCK"); +} + +=pod + +=item artist( [ $new_artist ] ) + +Returns the artist name; it is the first existing frame from the list of + + TPE1 Lead artist/Lead performer/Soloist/Performing group + TPE2 Band/Orchestra/Accompaniment + TCOM Composer + TPE3 Conductor + TEXT Lyricist/Text writer + +Sets TPE1 frame if given the optional arguments @new_artist. If this is an +empty string, the frame is removed. + +=cut + +sub artist { + my $self = shift; + if (@_) { + $self->remove_frame('TPE1') if defined $self->get_frame( "TPE1"); + return if @_ == 1 and $_[0] eq ''; + return $self->add_frame('TPE1', @_); + } + my $a; + ($a) = $self->get_frame("TPE1") and return $a; + ($a) = $self->get_frame("TPE2") and return $a; + ($a) = $self->get_frame("TCOM") and return $a; + ($a) = $self->get_frame("TPE3") and return $a; + ($a) = $self->get_frame("TEXT") and return $a; + return; +} + +=pod + +=item album( [ $new_album ] ) + +Returns the album name (TALB) from the tag. If none is found, returns +the "Content group description" (TIT1) frame (unless it is considered a part +of the title). + +Sets TALB frame if given the optional arguments @new_album. If this is an +empty string, the frame is removed. + +=cut + +sub album { + my $self = shift; + if (@_) { + $self->remove_frame('TALB') if defined $self->get_frame( "TALB"); + return if @_ == 1 and $_[0] eq ''; + return $self->add_frame('TALB', @_); + } + my $a; + ($a) = $self->get_frame("TALB") and return $a; + return if grep $_ eq 'TIT1', $self->v2title_order; + return scalar $self->get_frame("TIT1"); +} + +=item genre( [ $new_genre ] ) + +Returns the genre string from TCON frame of the tag. + +Sets TCON frame if given the optional arguments @new_genre. If this is an +empty string, the frame is removed. + +=cut + +sub genre { + my $self = shift; + if (@_) { + $self->remove_frame('TCON') if defined $self->get_frame( "TCON"); + return if @_ == 1 and $_[0] eq ''; + return $self->add_frame('TCON', @_); # XXX add genreID 0x00 ? + } + my $g = $self->get_frame('TCON'); + return unless defined $g; + $g =~ s/^\d+\0(?:.)//s; # XXX Shouldn't this be done in TCON()? + $g; +} + +=item version() + + $version = $id3v2->version(); + ($major, $revision) = $id3v2->version(); + +Returns the version of the ID3v2 tag. It returns a formatted string +like "3.0" or an array containing the major part (eg. 3) and revision +part (eg. 0) of the version number. + +=cut + +sub version { + my ($self) = @_; + if (wantarray) { + return ($self->{major}, $self->{revision}); + } else { + return $self->{version}; + } +} + +=item new() + + $tag = new($mp3fileobj); + +C<new()> needs as parameter a mp3fileobj, as created by C<MP3::Tag::File>. +C<new> tries to find a ID3v2 tag in the mp3fileobj. If it does not find a +tag it returns undef. Otherwise it reads the tag header, as well as an +extended header, if available. It reads the rest of the tag in a +buffer, does unsynchronizing if necessary, and returns a +ID3v2-object. At this moment only ID3v2.3 is supported. Any extended +header with CRC data is ignored, so no CRC check is done at the +moment. The ID3v2-object can be used to extract information from +the tag. + +Please use + + $mp3 = MP3::Tag->new($filename); + $mp3->get_tags(); ## to find an existing tag, or + $id3v2 = $mp3->new_tag("ID3v2"); ## to create a new tag + +instead of using this function directly + +=cut + +sub new { + my ($class, $mp3obj, $create, $r_header) = @_; + my $self={mp3=>$mp3obj}; + my $header=0; + bless $self, $class; + + if (defined $mp3obj) { # Not fake + $mp3obj->open or return unless $mp3obj->is_open; + $mp3obj->seek(0,0); + $mp3obj->read(\$header, 10); + $$r_header = $header if $r_header and 10 == length $header; + } + $self->{frame_start}=0; + # default ID3v2 version + $self->{major}=3; + $self->{frame_major}=3; # major for new frames + $self->{revision}=0; + $self->{version}= "$self->{major}.$self->{revision}"; + + if (defined $mp3obj and $self->read_header($header)) { + if ($create) { + $self->{tag_data} = ''; + $self->{data_size} = 0; + } else { + # sanity check: + my $s = $mp3obj->size; + my $s1 = $self->{tagsize} + $self->{footer_size}; + if (defined $s and $s - 10 < $s1) { + warn "Ridiculously large tag size: $s1; file size $s"; + return; + } + $mp3obj->read(\$self->{tag_data}, $s1); + $self->{data_size} = $self->{tagsize}; + $self->{raw_data} = $header . $self->{tag_data}; + # un-unsynchronize comes in all versions first + if ($self->{flags}->{unsync}) { + my $hits = $self->{tag_data} =~ s/\xFF\x00/\xFF/gs; + $self->{data_size} -= $hits; + } + # in v2.2.x complete tag may be compressed, but compression isn't + # described in tag specification, so get out if compression is found + if ($self->{flags}->{compress_all}) { + # can we test if it is simple zlib compression and use this? + warn "ID3v".$self->{version}." [whole tag] compression isn't supported. Cannot read tag\n"; + return undef; + } + # read the ext header if it exists + if ($self->{flags}->{extheader}) { + $self->{extheader} = substr ($self->{tag_data}, 0, 14); + unless ($self->read_ext_header()) { + return undef; # ext header not supported + } + } + $self->{footer} = substr $self->{tag_data}, -$self->{footer_size} + if $self->{footer_size}; + # Treat (illegal) padding after the tag + my($merge, $d, $z, $r) = ($mp3obj->get_config('id3v2_mergepadding'))->[0]; + my $max0s = $merge ? 1e100 : 16*1024; + while ($max0s and $mp3obj->read(\$d, 1024)) { + $max0s -= length $d; + ($z) = ($d =~ /^(\0*)/); + $self->{buggy_padding_size} += length $z if $merge; + ($r = substr $d, length $z), last unless length($z) == length($d); + } + $self->{tagend_offset} = $mp3obj->tell() - length $r; + $mp3obj->read(\$d, 10 - length $r) and $r .= $d if defined $r and length $r < 10; + $$r_header = $d if $r_header and 10 <= length $d; + } + $mp3obj->close; + return $self; + } else { + $mp3obj->close if defined $mp3obj; + if (defined $create && $create) { + $self->{tag_data}=''; + $self->{tagsize} = -10; + $self->{data_size} = 0; + $self->{buggy_padding_size} = 0; + return $self; + } + } + return undef; +} + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + my $header; + my $new = $class->new($filename, undef, \$header); + $parent->[0]{header} = $header if $header and $parent; + return unless $new; + $new->{parent} = $parent; + $new; +} + +################## +## +## internal subs +## + +# This sub tries to read the header of an ID3v2 tag and checks for the right header +# identification for the tag. It reads the version number of the tag, the tag size +# and the flags. +# Returns true if it finds a known ID3v2.x header, false otherwise. + +sub read_header { + my ($self, $header) = @_; + my %params; + + if (substr ($header,0,3) eq "ID3") { + # flag meaning for all supported ID3v2.x versions + my @flag_meaning=([],[], # v2.0 and v2.1 aren't supported yet + # 2.2 + ["unknown","unknown","unknown","unknown","unknown","unknown","compress_all","unsync"], + # 2.3 + ["unknown","unknown","unknown","unknown","unknown","experimental","extheader","unsync"], + # 2.4 + ["unknown","unknown","unknown","unknown","footer","experimental","extheader","unsync"], + # ???? + #["unknown","unknown","unknown","unknown","footer","experimental","extheader","unsync"], + ); + + # extract the header data + my ($major, $revision, $pflags) = unpack ("x3CCC", $header); + # check the version + if ($major > $#supported_majors or $supported_majors[$major] == 0) { + my $warn = "Unknown ID3v2-Tag version: v2.$major.$revision\n"; + $warn .= "| $major > ".($#supported_majors)." || $supported_majors[$major] == 0\n"; + + if($major > $#supported_majors) { + $warn .= "| major $major > ".($#supported_majors)."\n"; + } else { + $warn .= "| \$supported_majors[major=$major] == 0\n"; + } + $warn .= "$_: \$supported_majors[$_] = $supported_majors[$_]\n" + for (0..$#supported_majors); + warn $warn; + return 0; + } + if ($major == 4 and $self->get_config1('prohibit_v24')) { + warn "Reading ID3v2-Tag version: v2.$major.$revision is prohibited via setting `prohibit_v24'\n"; + return 0; + } + if ($revision != 0) { + warn "Unknown ID3v2-Tag revision: v2.$major.$revision\nTrying to read tag\n"; + } + # check the flags + my $flags={}; + my $unknownFlag=0; + my $i=0; + foreach (split (//, unpack('b8',pack('v',$pflags)))) { + $flags->{$flag_meaning[$major][$i]}=1 if $_; + $i++; + } + $self->{version} = "$major.$revision"; + $self->{major} = $major; + $self->{revision} = $revision; + # 2.3: includes extHeader, frames (as written), and the padding + # excludes the header size (10) + # 2.4: also excludes the footer (10 if present) + $self->{tagsize} = un_syncsafe_4bytes substr $header, 6, 4; + $self->{buggy_padding_size} = 0; # Fake so far + $self->{flags} = $flags; + $self->{footer_size} = ($self->{flags}->{footer} ? 10 : 0); + return 1; + } + return 0; # no ID3v2-Tag found +} + +# Reads the extended header and adapts the internal counter for the start of the +# frame data. Ignores the rest of the ext. header (as CRC data). + +# v2.3: +# Total size - 4 (4bytes, 6 or 10), flags (2bytes), padding size (4bytes), +# OptionalCRC. +# Flags: (subject to unsyncronization) +# %x0000000 00000000 +# x - CRC data present + +#If this flag is set four bytes of CRC-32 data is appended to the extended header. The CRC +#should be calculated before unsynchronisation on the data between the extended header and +#the padding, i.e. the frames and only the frames. +# Total frame CRC $xx xx xx xx + +# v2.4: Total size (4bytes, unsync), length of flags (=1), flags, Optional part. +# 2.4 flags (with the corresponding "Optional part" format): +# %0bcd0000 +# b - Tag is an update +# Flag data length $00 +# c - CRC data present +# Flag data length $05 +# Total frame CRC 5 * %0xxxxxxx +# d - Tag restrictions +# Flag data length $01 +# Restrictions %ppqrrstt + +sub read_ext_header { # XXXX in 2.3, it should be unsyncronized + my $self = shift; + my $ext_header = $self->{extheader}; + # flags, padding and crc ignored at this time + my $size; + if ($self->{major}==4) { + $size = un_syncsafe_4bytes substr $ext_header, 0, 4; + } else { # 4 bytes extra for the size field itself + $size = 4 + unpack("N", $ext_header); + } + $self->{frame_start} += $size; + return 1; +} + +sub extract_data { # Main sub for getting data from a frame + my ($self, $data, $format, $noDecode, $arr) = @_; + my ($rule, $found,$encoding, @result, $e); + + $encoding=0; + $arr ||= 0; # 1: values only; 2: return array + foreach $rule (@$format) { + next if exists $rule->{v3name}; + last if $rule->{optional} and not length $data; + # get the data + if ( exists $rule->{mlen} ) { # minlength, data is string + ($found, $data) = ($data, ""); # Never with encoding + } elsif ( $rule->{len} == 0 ) { # Till \0 + if (exists $rule->{encoded} && ($encoding =~ /^[12]$/)) { + ($found, $data) = ($data =~ /^((?:..)*?)(?:\0\0(.*)|\z)/s); + } else { + ($found, $data) = split /\x00/, $data, 2; + } + } elsif ($rule->{len} == -1) { # Till end + ($found, $data) = ($data, ""); + } else { + $found = substr $data, 0,$rule->{len}; + substr ($data, 0,$rule->{len}) = ''; + } + + # was data found? + unless (defined $found && $found ne "") { + $found = ""; + $found = $rule->{default} if exists $rule->{default}; + } + + # work with data + if ($rule->{name} eq "_encoding") { + $encoding=unpack ("C", $found); + push @result, 'encoding' unless $arr == 1; + push @result, $encoding; + } else { + if (exists $rule->{encoded}) { # decode data + if ( $encoding > 3 ) { + warn "Encoding type '$encoding' not supported: found in $rule->{name}\n"; + next; + } elsif ($encoding and not $trustencoding) { + warn "UTF encoding types disabled via MP3TAG_DECODE_UNICODE): found in $rule->{name}\n"; + next; + } elsif ($encoding) { + # 0 = latin1 (effectively: unknown) + # 1 = UTF-16 with BOM + # 2 = UTF-16be, no BOM + # 3 = UTF-8 + require Encode; + if ($decode_utf8) { + $found = Encode::decode($dec_types[$encoding], + $found); + } elsif ($encoding < 3) { + # Reencode in UTF-8 + $found = Encode::decode($dec_types[$encoding], + $found); + $found = Encode::encode('UTF-8', $found); + } + } elsif (not $noDecode and $e = $self->botched_encoding) { + require Encode; + $found = Encode::decode( $e, $found ); + } + } + + $found = toNumber($found) if $rule->{isnum}; + + unless ($arr) { + $found = $rule->{func}->($found) if exists $rule->{func}; + + unless (exists $rule->{data} || !defined $found) { + $found =~ s/[\x00]+$//; # some progs pad text fields with \x00 + $found =~ s![\x00]! / !g; # some progs use \x00 inside a text string to seperate text strings + $found =~ s/ +$//; # no trailing spaces after the text + } + + if (exists $rule->{re2}) { + while (my ($pat, $rep) = each %{$rule->{re2}}) { + $found =~ s/$pat/$rep/gis; + } + } + } + # store data + push @result, $rule->{name} unless $arr == 1; + push @result, $found; + } + } + return {@result} unless $arr; + return \@result; +} + +sub botched_encoding ($) { + my($self) = @_; + return if $self->{fixed_encoding}; + return unless my $enc = $self->get_config1('decode_encoding_v2'); + # Don't recourse into TXXX[*] (inside-[] is encoded, + # and frame_select() reads ALL TXXX frames...) + local $self->{fixed_encoding} = 1; + return unless $self->get_config1('ignore_trusted_encoding0_v2') + or not $self->frame_select('TXXX', 'trusted_encoding0_v2'); + $enc; +} + +# Make editing in presence of decode_encoding_v2 more predictable: +sub frames_need_fix_encoding ($) { + my($self) = @_; + return unless $self->botched_encoding; + my($fname, $rule, %fix); + for $fname (keys %{$self->{frames}}) { + my $frame = $self->{frames}->{$fname}; + next unless defined $frame; # XXXX Needed? + my $fname4 = substr ($fname, 0, 4); + my($result, $e) = $frame->{data}; + my $format = get_format($fname4); + next unless defined $format; + foreach $rule (@$format) { + next if exists $rule->{v3name}; + # Otherwise _encoding is the first entry + last if $rule->{name} ne '_encoding'; + $e = unpack ("C", $result); + } + next unless defined $e and not $e; # The unfortunate "latin1" + my $txts = $self->get_frame($fname, 'array_nokey'); + my $raw_txts = $self->get_frame($fname, 'array_nodecode'); + $fix{$fname} = $txts # Really need to fix: + if join("\0\0\0", @$txts) ne join("\0\0\0", @$raw_txts); + } + return unless %fix; + \%fix; +} + +sub fix_frames_encoding ($) { # do not touch frames unless absolutely needed + my($self) = @_; + my($fix, $fname, $txt) = $self->frames_need_fix_encoding; + while (($fname, $txt) = each %{$fix || {}}) { + shift @$txt; # The 1st field is always _encoding; recalculate it + $self->change_frame($fname, @$txt) or die; + } + $self->{fixed_encoding} = 1; + $self->frame_select('TXXX', 'trusted_encoding0_v2', undef, 1) + if $self->get_config1('id3v2_set_trusted_encoding0'); + return($fix and keys %$fix); # Better be scalar context... +} + +#Searches for a format string for a specified frame. format strings exist for +#specific frames, or also for a group of frames. Specific format strings have +#precedence over general ones. + +sub get_format { + my $fname = shift; + # to be quiet if called from supported_frames or what_data + my $quiet = shift; + my $fnamecopy = $fname; + while ($fname ne "") { + return $format{$fname} if exists $format{$fname}; + substr ($fname, -1) =""; #delete last char + } + warn "Unknown Frame-Format found: $fnamecopy\n" unless defined $quiet; + return undef; +} + +#Reads the flags of a frame, and returns a hash with all flags as keys, and +#0/1 as value for unset/set. +sub check_flags { + # how to detect unknown flags? + my ($self, $flags)=@_; + # %0abc0000 %0h00kmnp (this is byte1 byte2) + my @flagmap4 = qw/data_length unsync encryption compression unknown_j unknown_i groupid 0 + unknown_g unknown_f unknown_e unknown_d read_only file_preserv tag_preserv 0/; + # %abc00000 %ijk00000 + my @flagmap3 = qw/unknown_o unknown_n unknown_l unknown_m unknown_l groupid encryption compression + unknown_h unknown_g unknown_f unknown_e unknown_d read_only file_preserv tag_preserv/; + # flags were unpacked with 'n', so pack('v') gives byte2 byte1 + # unpack('b16') puts more significant bits to the right, separately for + # each byte; so the order is as specified above +# 2.4: +# %0abc0000 %0h00kmnp (this is byte1 byte2) +# a - Tag alter preservation +# b - File alter preservation +# c - Read only +# h - Grouping identity +# k - Compression +# m - Encryption +# n - Unsynchronisation +# p - Data length indicator +# 2.3: +# %abc00000 %ijk00000 +# a - Tag alter preservation +# b - File alter preservation +# c - Read only +# i - Compression +# j - Encryption +# k - Grouping identity + my @flagmap = $self->{major} == 4 ? @flagmap4 : @flagmap3; + my %flags = map { (shift @flagmap) => $_ } split (//, unpack('b16',pack('v',$flags))); + $flags{unchanged}=1; + return \%flags; +} + +sub build_flags { + my %flags=@_; + my $flags=0; + my %flagmap=(groupid=>32, encryption=>64, compression=>128, + read_only=>8192, file_preserv=>16384, tag_preserv=>32768); + while (my($flag,$set)=each %flags) { + if ($set and exists $flagmap{$flag}) { + $flags += $flagmap{$flag}; + } elsif (not exists $flagmap{$flag}) { + warn "Unknown flag during tag write: $flag\n"; + } + } + return $flags; +} + +sub DESTROY { +} + + +################################## +# +# How to store frame formats? +# +# format{fname}=[{xxx},{xxx},...] +# +# array containing descriptions of the different parts of a frame. Each description +# is a hash, which contains information, how to read the part. +# +# As Example: TCON +# Text encoding $xx +# Information <text string according to encoding +# +# TCON consist of two parts, so a array with two hashes is needed to describe this frame. +# +# A hash may contain the following keys. +# +# * len - says how many bytes to read for this part. 0 means read until \x00, -1 means +# read until end of frame, any value > 0 specifies an exact length +# * mlen - specifies a minimum length for the data, real length is until end of frame +# (we assume it is not paired with encoding) +# * name - the user sees this part of the frame under this name. If this part contains +# binary data, the name should start with a _ +# The name "_encoding" is reserved for the encoding part of a frame, which +# is handled specifically to support encoding of text strings +# (Is assumed to be the first entry, unless v3name) +# * encoded - this part has to be encoded following to the encoding information +# * func - a reference to a sub, which is called after the data is extracted. It gets +# this data as argument and has to return some data, which is then returned +# a result of this part +# * isnum=1 - indicator that field stores a number as binary number +# * re2 - hash with information for a replace: s/key/value/ +# This is used after a call of `func' when reading a frame +# * re2b - hash with information for a replace: s/key/value/ +# This is used when adding a frame +# * func_back - Translator function for add_frame (after re2b). +# * data=1 - indicator that this part contains binary data +# * default - default value, if data contains no information +# +# Name and exactly one of len or mlen are mandatory. +# +# TCON example: +# +# $format{TCON}=[{len=> 1, name=>"encoding", data=>1}, +# {len=>-1, name=>"text", func=>\&TCON, re2=>{'\(RX\)'=>'Remix', '\(CR\)'=>'Cover'}] +# +############################ + +sub toNumber { + my $num = 0; + $num = (256*$num)+unpack("C",$_) for split("",shift); + + return $num; +} + +sub APIC { # MAX about 20 + my $byte = shift; + my $index = unpack ("C", $byte); + my @pictypes = ("Other", "32x32 pixels 'file icon' (PNG only)", "Other file icon", + "Cover (front)", "Cover (back)", "Leaflet page", + "Media (e.g. label side of CD)", "Lead artist/lead performer/soloist", + "Artist/performer", "Conductor", "Band/Orchestra", "Composer", + "Lyricist/text writer", "Recording Location", "During recording", + "During performance", "Movie/video screen capture", + "A bright coloured fish", "Illustration", "Band/artist logotype", + "Publisher/Studio logotype"); + my $how = shift; + if (defined $how) { # called by what_data + die unless $how eq 1 and $byte eq 1; + my $c=0; + my %ret = map {$_, chr($c++)} @pictypes; + return \%ret; + } + # called by extract_data + return "Unknown... Error?" if $index > $#pictypes; + return $pictypes[$index]; +} + +sub COMR { # MAX about 9 + my $data = shift; + my $number = unpack ("C", $data); + my @receivedas = ("Other","Standard CD album with other songs", + "Compressed audio on CD","File over the Internet", + "Stream over the Internet","As note sheets", + "As note sheets in a book with other sheets", + "Music on other media","Non-musical merchandise"); + my $how = shift; + if (defined $how) { + die unless $how eq 1 and $data eq 1; + my $c=0; + my %ret = map {$_, chr($c++)} @receivedas; + return \%ret; + } + return $number if ($number>8); + return $receivedas[$number]; +} + +sub PIC { + # ID3v2.2 stores only 3 character Image format for pictures + # and not mime type: Convert image format to mime type + my $data = shift; + + my $how = shift; + if (defined $how) { # called by what_data + die unless $how eq 1 and $data eq 1; + my %ret={}; + return \%ret; + } + # called by extract_data + if ($data eq "-->") { + warn "ID3v2.2 PIC frame with link not supported\n"; + $data = "text/plain"; + } else { + $data = "image/".(lc $data); + } + return $data; +} + +sub TCON { + my $data = shift; + my $how = shift; + if (defined $how) { # called by what_data + die unless $how eq 1 and $data eq 1; + my $c=0; + my %ret = map {$_, "(".$c++.")"} @{MP3::Tag::ID3v1::genres()}; + $ret{"_FREE"}=1; + $ret{Remix}='(RX)'; + $ret{Cover}="(CR)"; + return \%ret; + } # called by extract_data + join ' / ', MP3::Tag::Implemenation::_massage_genres($data); +} + +sub TCON_back { + my $data = shift; + $data = join ' / ', map MP3::Tag::Implemenation::_massage_genres($_, 'prefer_num'), + split ' / ', $data; + $data =~ s[(?:(?<=\(\d\))|(?<=\(\d\d\d\))|(?<=\((?:RX|CV|\d\d)\))) / ][]ig; + $data =~ s[ / (?=\((?:RX|CV|\d{1,3})\))][]ig; + $data; +} + +sub TFLT { + my $text = shift; + my $how = shift; + if (defined $how) { # called by what_data + die unless $how eq 1 and $text eq 1; + my %ret=("MPEG Audio"=>"MPG", + "MPEG Audio MPEG 1/2 layer I"=>"MPG /1", + "MPEG Audio MPEG 1/2 layer II"=>"MPG /2", + "MPEG Audio MPEG 1/2 layer III"=>"MPG /3", + "MPEG Audio MPEG 2.5"=>"MPG /2.5", + "Transform-domain Weighted Interleave Vector Quantization"=>"VQF", + "Pulse Code Modulated Audio"=>"PCM", + "Advanced audio compression"=>"AAC", + "_FREE"=>1, + ); + return \%ret; + } + #called by extract_data + return "" if $text eq ""; + $text =~ s/MPG/MPEG Audio/; + $text =~ s/VQF/Transform-domain Weighted Interleave Vector Quantization/; + $text =~ s/PCM/Pulse Code Modulated Audio/; + $text =~ s/AAC/Advanced audio compression/; + unless ($text =~ s!/1!MPEG 1/2 layer I!) { + unless ($text =~ s!/2!MPEG 1/2 layer II!) { + unless ($text =~ s!/3!MPEG 1/2 layer III!) { + $text =~ s!/2\.5!MPEG 2.5!; + } + } + } + return $text; +} + +sub TMED { + #called by extract_data + my $text = shift; + return "" if $text eq ""; + if ($text =~ /(?<!\() \( ([\w\/]*) \) /x) { + my $found = $1; + if ($found =~ s!DIG!Other digital Media! || + $found =~ /DAT/ || + $found =~ /DCC/ || + $found =~ /DVD/ || + $found =~ s!MD!MiniDisc! || + $found =~ s!LD!Laserdisc!) { + $found =~ s!/A!, Analog Transfer from Audio!; + } + elsif ($found =~ /CD/) { + $found =~ s!/DD!, DDD!; + $found =~ s!/AD!, ADD!; + $found =~ s!/AA!, AAD!; + } + elsif ($found =~ s!ANA!Other analog Media!) { + $found =~ s!/WAC!, Wax cylinder!; + $found =~ s!/8CA!, 8-track tape cassette!; + } + elsif ($found =~ s!TT!Turntable records!) { + $found =~ s!/33!, 33.33 rpm!; + $found =~ s!/45!, 45 rpm!; + $found =~ s!/71!, 71.29 rpm!; + $found =~ s!/76!, 76.59 rpm!; + $found =~ s!/78!, 78.26 rpm!; + $found =~ s!/80!, 80 rpm!; + } + elsif ($found =~ s!TV!Television! || + $found =~ s!VID!Video! || + $found =~ s!RAD!Radio!) { + $found =~ s!/!, !; + } + elsif ($found =~ s!TEL!Telephone!) { + $found =~ s!/I!, ISDN!; + } + elsif ($found =~ s!REE!Reel! || + $found =~ s!MC!MC (normal cassette)!) { + $found =~ s!/4!, 4.75 cm/s (normal speed for a two sided cassette)!; + $found =~ s!/9!, 9.5 cm/s!; + $found =~ s!/19!, 19 cm/s!; + $found =~ s!/38!, 38 cm/s!; + $found =~ s!/76!, 76 cm/s!; + $found =~ s!/I!, Type I cassette (ferric/normal)!; + $found =~ s!/II!, Type II cassette (chrome)!; + $found =~ s!/III!, Type III cassette (ferric chrome)!; + $found =~ s!/IV!, Type IV cassette (metal)!; + } + $text =~ s/(?<!\() \( ([\w\/]*) \)/$found/x; + } + $text =~ s/\(\(/\(/g; + $text =~ s/ / /g; + + return $text; +} + +for my $elt ( qw( cddb_id cdindex_id ) ) { + no strict 'refs'; + *$elt = sub (;$) { + my $self = shift; + $self->frame_select('TXXX', $elt); + } +} + +BEGIN { + # ID3v2.2, v2.3 are supported, v2.4 is very compatible... + @supported_majors=(0,0,1,1,1); + + my $encoding ={len=>1, name=>"_encoding", data=>1}; + my $text_enc ={len=>-1, name=>"Text", encoded=>1}; + my $text ={len=>-1, name=>"Text"}; + my $description ={len=>0, name=>"Description", encoded=>1}; + my $url ={len=>-1, name=>"URL"}; + my $url0 ={len=>0, name=>"URL"}; + my $data ={len=>-1, name=>"_Data", data=>1}; + my $language ={len=>3, name=>"Language"}; + + # this list contains all id3v2.2 frame names which can be matched directly to a id3v2.3 frame + %v2names_to_v3 = ( + BUF => "RBUF", + CNT => "PCNT", + COM => "COMM", + CRA => "AENC", + EQU => "EQUA", + ETC => "ETCO", + GEO => "GEOB", + IPL => "IPLS", + MCI => "MDCI", + MLL => "MLLT", + POP => "POPM", + REV => "RVRB", + RVA => "RVAD", + SLT => "SYLT", + STC => "SYTC", + TFT => "TFLT", + TMT => "TMED", + UFI => "UFID", + ULT => "USLT", + TAL => "TALB", + TBP => "TBPM", + TCM => "TCOM", + TCO => "TCON", + TCR => "TCOP", + TDA => "TDAT", + TDY => "TDLY", + TEN => "TENC", + TIM => "TIME", + TKE => "TKEY", + TLA => "TLAN", + TLE => "TLEN", + TOA => "TOPE", + TOF => "TOFN", + TOL => "TOLY", + TOR => "TORY", + TOT => "TOAL", + TP1 => "TPE1", + TP2 => "TPE2", + TP3 => "TPE3", + TP4 => "TPE4", + TPA => "TPOS", + TPB => "TPUB", + TRC => "TSRC", + TRD => "TRDA", + TRK => "TRCK", + TSI => "TSIZ", + TSS => "TSSE", + TT1 => "TIT1", + TT2 => "TIT2", + TT3 => "TIT3", + TXT => "TEXT", + TXX => "TXXX", + TYE => "TYER", + WAF => "WOAF", + WAR => "WOAR", + WAS => "WOAS", + WCM => "WCOM", + WCP => "WCOP", + WPB => "WPUB", + WXX => "WXXX", + ); + + %format = ( + AENC => [$url0, {len=>2, name=>"Preview start", isnum=>1}, + {len=>2, name=>"Preview length", isnum=>1}, $data], + APIC => [$encoding, {len=>0, name=>"MIME type"}, + {len=>1, name=>"Picture Type", small_max=>1, func=>\&APIC}, + $description, $data], + COMM => [$encoding, $language, $description, $text_enc], + COMR => [$encoding, {len=>0, name=>"Price"}, + {len=>8, name=>"Valid until"}, $url0, + {len=>1, name=>"Received as", small_max=>1, func=>\&COMR}, + {len=>0, name=>"Name of Seller", encoded=>1}, + $description, {len=>0, name=>"MIME type", optional=>1}, + {len=>-1, name=>"_Logo", data=>1, optional => 1}], + CRM => [{v3name=>""},{len=>0, name=>"Owner ID"}, {len=>0, name=>"Content/explanation"}, $data], #v2.2 + ENCR => [{len=>0, name=>"Owner ID"}, {len=>0, name=>"Method symbol"}, $data], + #EQUA => [], + #ETCO => [], + GEOB => [$encoding, {len=>0, name=>"MIME type"}, + {len=>0, name=>"Filename"}, $description, $data], + GRID => [{len=>0, name=>"Owner"}, {len=>1, name=>"Symbol", isnum=>1}, + $data], + IPLS => [$encoding, $text_enc], # in 2.4 split into TMCL, TIPL + LNK => [{len=>4, name=>"ID", func=>\&LNK}, {len=>0, name=>"URL"}, $text], + LINK => [{len=>4, name=>"ID"}, {len=>0, name=>"URL"}, $text], + MCDI => [$data], + #MLLT => [], + OWNE => [$encoding, {len=>0, name=>"Price payed"}, + {len=>0, name=>"Date of purchase"}, $text_enc], + PCNT => [{mlen=>4, name=>"Text", isnum=>1}], + PIC => [{v3name => "APIC"}, $encoding, {len=>3, name=>"Image Format", func=>\&PIC}, + {len=>1, name=>"Picture Type", func=>\&APIC}, $description, $data], #v2.2 + POPM => [{len=>0, name=>"URL"},{len=>1, name=>"Rating", isnum=>1}, + {mlen=>4, name=>"Counter", isnum=>1, optional=>1}], + #POSS => [], + PRIV => [{len=>0, name=>"Text"}, $data], + RBUF => [{len=>3, name=>"Buffer size", isnum=>1}, + {len=>1, name=>"Embedded info flag", isnum=>1}, + {len=>4, name=>"Offset to next tag", isnum=>1, optional=>1}], + #RVAD => [], + RVRB => [{len=>2, name=>"Reverb left (ms)", isnum=>1}, + {len=>2, name=>"Reverb right (ms)", isnum=>1}, + {len=>1, name=>"Reverb bounces (left)", isnum=>1}, + {len=>1, name=>"Reverb bounces (right)", isnum=>1}, + {len=>1, name=>"Reverb feedback (left to left)", isnum=>1}, + {len=>1, name=>"Reverb feedback (left to right)", isnum=>1}, + {len=>1, name=>"Reverb feedback (right to right)", isnum=>1}, + {len=>1, name=>"Reverb feedback (right to left)", isnum=>1}, + {len=>1, name=>"Premix left to right", isnum=>1}, + {len=>1, name=>"Premix right to left", isnum=>1},], + SYTC => [{len=>1, name=>"Time Stamp Format", isnum=>1}, $data], + #SYLT => [], + T => [$encoding, $text_enc], + TCON => [$encoding, + {%$text_enc, func=>\&TCON, func_back => \&TCON_back, + re2=>{'\(RX\)'=>'Remix', '\(CR\)'=>'Cover'}, + # re2b=>{'\bRemix\b'=>'(RX)', '\bCover\b'=>'(CR)'} + }], + TCOP => [$encoding, + {%$text_enc, re2 => {'^(?!\Z)'=>'(C) '}, + re2b => {'^(Copyright\b)?\s*(\(C\)\s*)?' => ''}}], + # TDRC => [$encoding, $text_enc, data => 1], + TFLT => [$encoding, {%$text_enc, func=>\&TFLT}], + TIPL => [{v3name => "IPLS"}, $encoding, $text_enc], + TMCL => [{v3name => "IPLS"}, $encoding, $text_enc], + TMED => [$encoding, {%$text_enc, func=>\&TMED}], # no what_data support + TXXX => [$encoding, $description, $text_enc], + UFID => [{%$description, name=>"Text"}, $data], + USER => [$encoding, $language, $text_enc], + USLT => [$encoding, $language, $description, $text_enc], + W => [$url], + WXXX => [$encoding, $description, $url], + ); + + %long_names = ( + AENC => "Audio encryption", + APIC => "Attached picture", + COMM => "Comments", + COMR => "Commercial frame", + ENCR => "Encryption method registration", + EQUA => "Equalization", + ETCO => "Event timing codes", + GEOB => "General encapsulated object", + GRID => "Group identification registration", + IPLS => "Involved people list", + LINK => "Linked information", + MCDI => "Music CD identifier", + MLLT => "MPEG location lookup table", + OWNE => "Ownership frame", + PRIV => "Private frame", + PCNT => "Play counter", + POPM => "Popularimeter", + POSS => "Position synchronisation frame", + RBUF => "Recommended buffer size", + RVAD => "Relative volume adjustment", + RVRB => "Reverb", + SYLT => "Synchronized lyric/text", + SYTC => "Synchronized tempo codes", + TALB => "Album/Movie/Show title", + TBPM => "BPM (beats per minute)", + TCOM => "Composer", + TCON => "Content type", + TCOP => "Copyright message", + TDAT => "Date", + TDLY => "Playlist delay", + TDRC => "Recording time", + TENC => "Encoded by", + TEXT => "Lyricist/Text writer", + TFLT => "File type", + TIME => "Time", + TIPL => "Involved people list", + TIT1 => "Content group description", + TIT2 => "Title/songname/content description", + TIT3 => "Subtitle/Description refinement", + TKEY => "Initial key", + TLAN => "Language(s)", + TLEN => "Length", + TMCL => "Musician credits list", + TMED => "Media type", + TOAL => "Original album/movie/show title", + TOFN => "Original filename", + TOLY => "Original lyricist(s)/text writer(s)", + TOPE => "Original artist(s)/performer(s)", + TORY => "Original release year", + TOWN => "File owner/licensee", + TPE1 => "Lead performer(s)/Soloist(s)", + TPE2 => "Band/orchestra/accompaniment", + TPE3 => "Conductor/performer refinement", + TPE4 => "Interpreted, remixed, or otherwise modified by", + TPOS => "Part of a set", + TPUB => "Publisher", + TRCK => "Track number/Position in set", + TRDA => "Recording dates", + TRSN => "Internet radio station name", + TRSO => "Internet radio station owner", + TSIZ => "Size", + TSRC => "ISRC (international standard recording code)", + TSSE => "Software/Hardware and settings used for encoding", + TYER => "Year", + TXXX => "User defined text information frame", + UFID => "Unique file identifier", + USER => "Terms of use", + USLT => "Unsychronized lyric/text transcription", + WCOM => "Commercial information", + WCOP => "Copyright/Legal information", + WOAF => "Official audio file webpage", + WOAR => "Official artist/performer webpage", + WOAS => "Official audio source webpage", + WORS => "Official internet radio station homepage", + WPAY => "Payment", + WPUB => "Publishers official webpage", + WXXX => "User defined URL link frame", + + # ID3v2.2 frames which cannot linked directly to a ID3v2.3 frame + CRM => "Encrypted meta frame", + PIC => "Attached picture", + LNK => "Linked information", + ); + + # these fields have restricted input (FRAMEfield) + %res_inp=( "APICPicture Type" => \&APIC, + "TCONText" => \&TCON, # Actually, has func_back()... + "TFLTText" => \&TFLT, + "COMRReceived as" => \&COMR, + ); + # have small_max + %is_small_int = ("APICPicture Type" => 1, "COMRReceived as" => 1); + + for my $k (keys %res_inp) { + my %h = %{ $field_map{$k} = $res_inp{$k}->(1,1) }; # Assign+make copy + delete $h{_FREE}; + %h = reverse %h; + $field_map_back{$k} = \%h; + } + # Watch for 'lable': + $field_map{'APICPicture Type'}{'Media (e.g. lable side of CD)'} = + $field_map{'APICPicture Type'}{'Media (e.g. label side of CD)'}; + %back_splt = qw(POPM 1); # Have numbers at end + %embedded_Descr = qw(GEOD 1 COMR 1); # Have descr which is not leading +} + +=pod + +=back + +=head1 BUGS + +Writing C<v2.4>-layout tags is not supported. + +Additionally, one should keep in mind that C<v2.3> and C<v2.4> have differences +in two areas: + +=over 4 + +=item * + +layout of information in the byte stream (in other words, in a file +considered as a string) is different; + +=item * + +semantic of frames is extended in C<v2.4> - more frames are defined, and +more frame flags are defined too. + +=back + +MP3::Tag does not even try to I<write> frames in C<v2.4>-layout. However, +when I<reading> the frames, MP3::Tag does not assume any restriction on +the semantic of frames - it allows all the semantical extensions +defined in C<v2.4> even for C<v2.3> (and, probably, for C<v2.2>) layout. + +C<[*]> (I expect, any sane program would do the same...) + +Likewise, when writing frames, there is no restriction imposed on semantic. +If user specifies a frame the meaning of which is defined only in C<v2.4>, +we would happily write it even when we use C<v2.3> layout. Same for frame +flags. (And given the assumption C<[*]>, this is a correct thing to do...) + +=head1 SEE ALSO + +L<MP3::Tag>, L<MP3::Tag::ID3v1>, L<MP3::Tag::ID3v2_Data> + +ID3v2 standard - http://www.id3.org +L<http://www.id3.org/id3v2-00>, L<http://www.id3.org/d3v2.3.0>, +L<http://www.id3.org/id3v2.4.0-structure>, +L<http://www.id3.org/id3v2.4.0-frames>, +L<http://id3lib.sourceforge.net/id3/id3v2.4.0-changes.txt>. + +=head1 COPYRIGHT + +Copyright (c) 2000-2008 Thomas Geffert, Ilya Zakharevich. All rights reserved. + +This program is free software; you can redistribute it and/or +modify it under the terms of the Artistic License, distributed +with Perl. + +=cut + + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/ID3v2_Data.pod b/fhem/FHEM/lib/MP3/Tag/ID3v2_Data.pod new file mode 100644 index 000000000..bf0d30b98 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ID3v2_Data.pod @@ -0,0 +1,332 @@ + +=head1 NAME + +MP3::Tag::ID3v2_Data - get_frame() data format and supported frames + +=head1 SYNOPSIS + + $mp3 = MP3::Tag->new($filename); + $mp3->get_tags(); + $id3v2 = $mp3->{ID3v2} if exists $mp3->{id3v2}; + + ($info, $long) = $id3v2->get_frame($id); # or + + ($info, $long) = $id3v2->get_frame($id, 'raw'); + + +=head1 DESCRIPTION + +This document describes how to use the results of the get_frame function of +MP3::Tag::ID3v2, thus the data format of frames retrieved with +MP3::Tag::ID3v2::get_frame(). + +It contains also a list of all supported ID3v2-Frames. + +=head2 get_frame() + + ($info, $long) = $id3v2->get_frame($id); # or + + ($info, $long) = $id3v2->get_frame($id, 'raw'); + +$id has to be a name of a frame like "APIC". For more variants of calling +see L<get_frame()|MP3::Tag::ID3v2>. + +The names of all frames found in a tag can be retrieved with the L<get_frame_ids()|MP3::Tag::ID3v2> function. + +=head2 Using the returned data + +In the ID3v2.3 specifications 73 frames are defined, which can contain very +different information. That means that get_frame returns the information +of different frames also in different ways. + +=over 4 + +=item Simple Frames + +A lot of the tags contain only a text string and encoding information. If +you call ($info, $long) = $id3v2->get_frame($id) for such a frame, $info will contain +the text string and $long will contain the english name of the frame. + +Example: + get_frame("TIT2"); # returns + + ("Birdhouse In Your Soul", "Title/songname/content description") + +=item Complex Frames + +For more complex frames the returned $info is a reference to a hash, where +each entry of the hash decribes a part of the information found in the +frame. The key of a hash entry contains the name of this part, the according +value contains the information itself. + +Example: + get_frame("APIC"); # returns + + ( { "Description" => "Flood", + "MIME Type" => "/image/jpeg", + "Picture Type" => "Cover (front)", + "_Data" => "..data of jpeg picture (binary).." + }, + "Attached Picture"); + +=item Other Frames + +Some frames are not supported at the moment, ie the data found in the frame +is not returned in a descriptive way. But you can read the data of this +frames (and also of all other frames too) in raw mode. Then the complete +data field of the frame is returned, without any modifications. This means +that the returned data will be almost binary data. + +Example: + get_frame("TIT2", 'raw'); # returns + + ("\x00Birdhouse In Your Soul", "Title/songname/content description") + +=back + +The frames which (in addition to C<Text>/C<URL>) contain only +C<Description> and C<Language> fields are in some intermediate position +between "simple" and "complex" frames. They can be handled very similarly +to "simple" frames by using "long names", such as C<COMM[description]> +or C<COMM(LANG)[description]>, and the corresponding "quick" API such +as frame_select(). + + + +=head2 List of Simple Frames + +Following Frames are supported +and return a single string (text). In the List you can find the frame IDs +and the long names of the frames as returned by $id3v2->get_frame(): + +=over 4 + + +=item IPLS : Involved people list + +=item MCDI : Music CD identifier + +=item PCNT : Play counter + +=item TALB : Album/Movie/Show title + +=item TBPM : BPM (beats per minute) + +=item TCOM : Composer + +=item TCON : Content type + +=item TCOP : Copyright message + +=item TDAT : Date + +=item TDLY : Playlist delay + +=item TDRC : Recording time + +=item TENC : Encoded by + +=item TEXT : Lyricist/Text writer + +=item TFLT : File type + +=item TIME : Time + +=item TIPL : Involved people list + +=item TIT1 : Content group description + +=item TIT2 : Title/songname/content description + +=item TIT3 : Subtitle/Description refinement + +=item TKEY : Initial key + +=item TLAN : Language(s) + +=item TLEN : Length + +=item TMCL : Musician credits list + +=item TMED : Media type + +=item TOAL : Original album/movie/show title + +=item TOFN : Original filename + +=item TOLY : Original lyricist(s)/text writer(s) + +=item TOPE : Original artist(s)/performer(s) + +=item TORY : Original release year + +=item TOWN : File owner/licensee + +=item TPE1 : Lead performer(s)/Soloist(s) + +=item TPE2 : Band/orchestra/accompaniment + +=item TPE3 : Conductor/performer refinement + +=item TPE4 : Interpreted, remixed, or otherwise modified by + +=item TPOS : Part of a set + +=item TPUB : Publisher + +=item TRCK : Track number/Position in set + +=item TRDA : Recording dates + +=item TRSN : Internet radio station name + +=item TRSO : Internet radio station owner + +=item TSIZ : Size + +=item TSRC : ISRC (international standard recording code) + +=item TSSE : Software/Hardware and settings used for encoding + +=item TYER : Year + +=item WCOM : Commercial information + +=item WCOP : Copyright/Legal information + +=item WOAF : Official audio file webpage + +=item WOAR : Official artist/performer webpage + +=item WOAS : Official audio source webpage + +=item WORS : Official internet radio station homepage + +=item WPAY : Payment + +=item WPUB : Publishers official webpage + +=back + + + +=head2 List of Complex Frames + +Following frames are supported and return a reference to a hash. The +list shows which keys can be found in the returned hash: + +=over 4 + + +=item AENC : Audio encryption + + Keys: URL, Preview start, Preview length, _Data + +=item APIC : Attached picture + + Keys: MIME type, Picture Type, Description, _Data + +=item COMM : Comments + + Keys: Language, Description, Text + +=item COMR : Commercial frame + + Keys: Price, Valid until, URL, Received as, Name of Seller, Description, MIME type, _Logo + +=item ENCR : Encryption method registration + + Keys: Owner ID, Method symbol, _Data + +=item GEOB : General encapsulated object + + Keys: MIME type, Filename, Description, _Data + +=item GRID : Group identification registration + + Keys: Owner, Symbol, _Data + +=item LINK : Linked information + + Keys: ID, URL, Text + +=item OWNE : Ownership frame + + Keys: Price payed, Date of purchase, Text + +=item POPM : Popularimeter + + Keys: URL, Rating, Counter + +=item PRIV : Private frame + + Keys: Text, _Data + +=item RBUF : Recommended buffer size + + Keys: Buffer size, Embedded info flag, Offset to next tag + +=item RVRB : Reverb + + Keys: Reverb left (ms), Reverb right (ms), Reverb bounces (left), Reverb bounces (right), Reverb feedback (left to left), Reverb feedback (left to right), Reverb feedback (right to right), Reverb feedback (right to left), Premix left to right, Premix right to left + +=item SYTC : Synchronized tempo codes + + Keys: Time Stamp Format, _Data + +=item TXXX : User defined text information frame + + Keys: Description, Text + +=item UFID : Unique file identifier + + Keys: Text, _Data + +=item USER : Terms of use + + Keys: Language, Text + +=item USLT : Unsychronized lyric/text transcription + + Keys: Language, Description, Text + +=item WXXX : User defined URL link frame + + Keys: Description, URL + +=back + + + +=head2 List of Other Frames + +Following frames are only supported in raw mode: + +=over 4 + + +=item CRM : Encrypted meta frame + +=item EQUA : Equalization + +=item ETCO : Event timing codes + +=item LNK : Linked information + +=item MLLT : MPEG location lookup table + +=item PIC : Attached picture + +=item POSS : Position synchronisation frame + +=item RVAD : Relative volume adjustment + +=item SYLT : Synchronized lyric/text + +=back + + +=head1 SEE ALSO + +L<MP3::Tag>, L<MP3::Tag::ID3v2> + diff --git a/fhem/FHEM/lib/MP3/Tag/ImageExifTool.pm b/fhem/FHEM/lib/MP3/Tag/ImageExifTool.pm new file mode 100644 index 000000000..909ac50b3 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ImageExifTool.pm @@ -0,0 +1,119 @@ +package MP3::Tag::ImageExifTool; + +use strict; +use File::Basename; +#use File::Spec; +use vars qw /$VERSION @ISA/; + +$VERSION="0.01"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::ImageExifTool - extract size info from image files via L<Image::Size|Image::Size>. + +=head1 SYNOPSIS + + my $db = MP3::Tag::ImageExifTool->new($filename); # Name of multimedia file + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::ImageExifTool is designed to be called from the MP3::Tag module. + +It implements width(), height() and mime_type() methods (sizes in pixels). + +They return C<undef> if C<Image::Size> is not available, or does not return valid data. + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $f, $p, $e, %seen, @cue) = (shift, shift, shift); + $f = $f->filename if ref $f; + bless [$f], $class; +} + +sub new { + my ($class, $f) = (shift, shift); + $class->new_with_parent($f, undef, @_); +} + +# Destructor + +sub DESTROY {} + +sub __info ($) { + my $self = shift; + unless (defined $self->[1]) { + my $v = eval { require Image::ExifTool; + Image::ExifTool->new()->ImageInfo($self->[0], '-id3:*') }; + # How to detect errors? + $self->[1] = $v->{Error} ? '' : $v; + } + return $self->[1]; +} + +my %tr = qw( mime_type MIMEType year Date width ImageWidth height ImageHeight + bit_depth BitDepth ); + +for my $elt ( qw( title track artist album year genre comment mime_type + width height ) ) { + my $n = ($tr{$elt} or ucfirst $elt); + my $is_genre = ($elt eq 'genre'); + my $r = sub ($) { + my $info = shift()->__info; + return unless $info; + my $v = $info->{$n}; + $v =~ s/^None$// if $is_genre and $v; + return $v; + }; + no strict 'refs'; + *$elt = $r; +} + +sub bit_depth ($) { + my $info = shift()->__info; + return unless $info; + $info->{BitsPerSample} || $info->{Depth} || $info->{BitDepth} +} + +sub field ($$) { + my $info = shift()->__info; + return unless $info; + $info->{shift()} +} + +sub _duration ($) { + my $info = shift()->__info; + return unless $info; + my($d, $dd) = $info->{Duration}; + if (defined $d and $d =~ /\d/) { + $dd = 1; + return $d if $d =~ /^\d*(\.\d*)?$/; + } + # Probably this is already covered by Duration? No, it is usually rounded... + my($c, $r, $r1) = map $info->{$_}, qw(FrameCount VideoFrameRate FrameRate); + unless (defined $c and $r ||= $r1) { # $d usually contains rounded value + return $1*3600 + $2*60 + $3 if $dd and $d =~ /^(\d+):(\d+):(\d+(\.\d*)?)$/; + return $1*60 + $2 if $dd and $d =~ /^(\d+):(\d+(\.\d*)?)$/; + return; + } + $r = 30/1.001 if $r =~ /^29.97\d*^/; + $r = 24/1.001 if $r =~ /^23.9(7\d*|8)$/; + $c/$r +} + +sub img_type ($) { + my $self = shift; + my $t = $self->mime_type; + return uc $1 if $t =~ m(^image/(.*)); + return; +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/ImageSize.pm b/fhem/FHEM/lib/MP3/Tag/ImageSize.pm new file mode 100644 index 000000000..de2be78ba --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ImageSize.pm @@ -0,0 +1,77 @@ +package MP3::Tag::ImageSize; + +use strict; +use File::Basename; +#use File::Spec; +use vars qw /$VERSION @ISA/; + +$VERSION="0.01"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::ImageSize - extract size info from image files via L<Image::Size|Image::Size>. + +=head1 SYNOPSIS + + my $db = MP3::Tag::ImageSize->new($filename); # Name of multimedia file + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::ImageSize is designed to be called from the MP3::Tag module. + +It implements width(), height() and mime_type() methods (sizes in pixels). + +They return C<undef> if C<Image::Size> is not available, or does not return valid data. + +=head1 SEE ALSO + +L<Image::Size>, L<MP3::Tag> + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $f, $p, $e, %seen, @cue) = (shift, shift, shift); + $f = $f->filename if ref $f; + bless [$f], $class; +} + +sub new { + my ($class, $f) = (shift, shift); + $class->new_with_parent($f, undef, @_); +} + +# Destructor + +sub DESTROY {} + +my @fields = qw( 0 0 width height img_type mime_type ); +for my $elt ( 2, 3, 4, 5 ) { # i_bitdepth + my $r = sub (;$) { + my $self = shift; + unless ($self->[1]) { + my ($w, $h, $t) = eval { require Image::Size; + Image::Size::imgsize($self->[0]) }; + defined $w or @$self[1..4] = (1,undef,undef,undef), return; + my $tt = "image/\L$t"; + @$self[1..5] = (1, $w, $h, $t, $tt); + } + return $self->[$elt]; + }; + no strict 'refs'; + *{$fields[$elt]} = $r; +} + +for my $elt ( qw( title track artist album year genre comment ) ) { + no strict 'refs'; + *$elt = sub (;$) { return }; +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/Inf.pm b/fhem/FHEM/lib/MP3/Tag/Inf.pm new file mode 100644 index 000000000..0be1b4679 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/Inf.pm @@ -0,0 +1,148 @@ +package MP3::Tag::Inf; + +use strict; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::Inf - Module for parsing F<.inf> files associated with music tracks. + +=head1 SYNOPSIS + + my $mp3inf = MP3::Tag::Inf->new($filename); # Name of MP3 or .INF file + # or an MP3::Tag::File object + + ($title, $artist, $album, $year, $comment, $track) = $mp3inf->parse(); + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::Inf is designed to be called from the MP3::Tag module. + +It parses the content of F<.inf> file (created, e.g., by cdda2wav). + +=over 4 + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + my $self = bless {parent => $parent}, $class; + + $filename = $filename->filename if ref $filename; + my $ext_rex = $self->get_config('extension')->[0]; + $filename =~ s/($ext_rex)|$/.inf/; # replace extension + return unless -f $filename; + $self->{filename} = $filename; + $self; +} + +# Destructor + +sub DESTROY {} + +=item parse() + + ($title, $artist, $album, $year, $comment, $track) = + $mp3inf->parse($what); + +parse_filename() extracts information about artist, title, track number, +album and year from the F<.inf> file. $what is optional; it maybe title, +track, artist, album, year or comment. If $what is defined parse() will return +only this element. + +As a side effect of this call, $mp3inf->{info} is set to the hash reference +with the content of particular elements of the F<.inf> file. Typically present +are the following fields: + + CDINDEX_DISCID + CDDB_DISCID + MCN + ISRC + Albumperformer + Performer + Albumtitle + Tracktitle + Tracknumber + Trackstart + Tracklength + Pre-emphasis + Channels + Copy_permitted + Endianess + Index + +The following fields are also recognized: + + Year + Trackcomment + +=cut + +sub return_parsed { + my ($self,$what) = @_; + if (defined $what) { + return $self->{parsed}{album} if $what =~/^al/i; + return $self->{parsed}{artist} if $what =~/^a/i; + return $self->{parsed}{track} if $what =~/^tr/i; + return $self->{parsed}{year} if $what =~/^y/i; + return $self->{parsed}{genre} if $what =~/^g/i; + if ($what =~/^cddb_id/i) { + my $o = $self->{parsed}{Cddb_discid}; + $o =~ s/^0x//i if $o; + return $o; + } + return $self->{parsed}{Cdindex_discid} if $what =~/^cdindex_id/i; + return $self->{parsed}{comment}if $what =~/^c/i; + return $self->{parsed}{title}; + } + + return $self->{parsed} unless wantarray; + return map $self->{parsed}{$_} , qw(title artist album year comment track); +} + +sub parse { + my ($self,$what) = @_; + + $self->return_parsed($what) if exists $self->{parsed}; + local *IN; + open IN, "< $self->{filename}" or die "Error opening `$self->{filename}': $!"; + my $e; + if ($e = $self->get_config('decode_encoding_inf') and $e->[0]) { + eval "binmode IN, ':encoding($e->[0])'"; # old binmode won't compile... + } + my ($line, %info); + for $line (<IN>) { + $self->{info}{ucfirst lc $1} = $2 + if $line =~ /^(\S+)\s*=\s*['"]?(.*?)['"]?\s*$/; + } + close IN or die "Error closing `$self->{filename}': $!"; + my %parsed; + @parsed{ qw( title artist album year comment track Cddb_discid Cdindex_discid ) } = + @{ $self->{info} }{ qw( Tracktitle Performer Albumtitle + Year Trackcomment Tracknumber + Cddb_discid Cdindex_discid) }; + $parsed{artist} = $self->{info}{Albumperformer} + unless defined $parsed{artist}; + $self->{parsed} = \%parsed; + $self->return_parsed($what); +} + +for my $elt ( qw( title track artist album comment year genre cddb_id cdindex_id ) ) { + no strict 'refs'; + *$elt = sub (;$) { + my $self = shift; + $self->parse($elt, @_); + } +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/LastResort.pm b/fhem/FHEM/lib/MP3/Tag/LastResort.pm new file mode 100644 index 000000000..e5f81f2e2 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/LastResort.pm @@ -0,0 +1,52 @@ +package MP3::Tag::LastResort; + +use strict; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::LastResort - Module for using other fields to fill autoinfo fields. + +=head1 SYNOPSIS + + my $mp3extra = MP3::Tag::LastResort::new_with_parent($filename, $parent); + $comment = $mp3inf->comment(); + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::LastResort is designed to be called from the MP3::Tag module. + +It uses the artist_collection() as comment() if comment() is not otherwise +defined. + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + bless {parent => $parent}, $class; +} + +# Destructor + +sub DESTROY {} + +for my $elt ( qw( title track artist album year genre ) ) { + no strict 'refs'; + *$elt = sub (;$) { return }; +} + +sub comment { + shift->{parent}->artist_collection() +} + +1; diff --git a/fhem/FHEM/lib/MP3/Tag/ParseData.pm b/fhem/FHEM/lib/MP3/Tag/ParseData.pm new file mode 100644 index 000000000..8c6f71f68 --- /dev/null +++ b/fhem/FHEM/lib/MP3/Tag/ParseData.pm @@ -0,0 +1,274 @@ +package MP3::Tag::ParseData; + +use strict; +use vars qw /$VERSION @ISA/; + +$VERSION="1.00"; +@ISA = 'MP3::Tag::__hasparent'; + +=pod + +=head1 NAME + +MP3::Tag::ParseData - Module for parsing arbitrary data associated with music files. + +=head1 SYNOPSIS + + # parses the file name according to one of the patterns: + $mp3->config('parse_data', ['i', '%f', '%t - %n - %a.%e', '%t - %y.%e']); + $title = $mp3->title; + +see L<MP3::Tag> + +=head1 DESCRIPTION + +MP3::Tag::ParseData is designed to be called from the MP3::Tag module. + +Each option of configuration item C<parse_data> should be of the form +C<[$flag, $string, $pattern1, ...]>. For each of the option, patterns of +the option are matched agains the $string of the option, until one of them +succeeds. The information obtained from later options takes precedence over +the information obtained from earlier ones. + +The meaning of the patterns is the same as for parse() or parse_rex() methods +of C<MP3::Tag>. Since the default for C<parse_data> is empty, by default this +handler has no effect. + +$flag is split into 1-character-long flags (unknown flags are ignored): + +=over + +=item C<i> + +the string-to-parse is interpolated first; + +=item C<f> + +the string-to-parse is interpreted as the name of the file to read; + +=item C<F> + +added to C<f>, makes it non-fatal if the file does not exist; + +=item C<B> + +the file should be read in C<binary> mode; + +=item C<n> + +the string-to-parse is interpreted as collection of lines, one per track; + +=item C<l> + +the string-to-parse is interpreted as collection of lines, and the first +matched is chosen; + +=item C<I> + +the resulting string is interpolated before parsing. + +=item C<b> + +Do not strip the leading and trailing blanks. (With output to file, +the output is performed in binary mode too.) + +=item C<R> + +the patterns are considered as regular expressions. + +=item C<m> + +one of the patterns must match. + +=item C<o>, C<O>, C<D> + +With C<o> or C<O> interpret the pattern as a name of file to output +parse-data to. With C<O> the name of output file is interpolated. +When C<D> is present, intermediate directories are created. + +=item C<z> + +Do not ignore a field even if the result is a 0-length string. + +=back + +Unless C<b> option is given, the resulting values have starting and +trailing whitespace trimmed. (Actually, split()ing into lines is done +using the configuration item C<parse_split>; it defaults to C<"\n">.) + +If the configuration item C<parse_data> has multiple options, the $strings +which are interpolated will use information set by preceding options; +similarly, any interolated option may use information obtained by other +handlers - even if these handers are later in the pecking order than +C<MP3::Tag::ParseData> (which by default is the first handler). For +example, with + + ['i', '%t' => '%t (%y)'], ['i', '%t' => '%t - %c'] + +and a local CDDB file which identifies title to C<'Merry old - another +interpretation (1905)'>, the first field will interpolate C<'%t'> into this +title, then will split it into the year and the rest. The second field will +split the rest into a title-proper and comment. + +Note that one can use fields of the form + + ['mz', 'This is a forced title' => '%t'] + +to force particular values for parts of the MP3 tag. + +The usual methods C<artist>, C<title>, C<album>, C<comment>, C<year>, C<track>, +C<year> can be used to access the results of the parse. + +It is possible to set individual id3v2 frames; use %{TIT1} or +some such. Setting to an empty string deletes the frame if config +parameter C<id3v2_frame_empty_ok> is false (the default value). +Setting ID3v2 frames uses the same translation rules as +select_id3v2_frame_by_descr(). + +=head2 SEE ALSO + +The flags C<i f F B l m I b> are identical to flags of the method +interpolate_with_flags() of MP3::Tag (see L<MP3::Tag/"interpolate_with_flags">). +Essentially, the other flags (C<R m o O D z>) are applied to the result of +calling the latter method. + +=cut + + +# Constructor + +sub new_with_parent { + my ($class, $filename, $parent) = @_; + $filename = $filename->filename if ref $filename; + bless {filename => $filename, parent => $parent}, $class; +} + +# Destructor + +sub DESTROY {} + +sub parse_one { + my ($self, $in) = @_; + + my @patterns = @$in; # Apply shift to a copy, not original... + my $flags = shift @patterns; + my $data = shift @patterns; + + my @data = $self->{parent}->interpolate_with_flags($data, $flags); + my $res; + my @opatterns = @patterns; + + if ($flags =~ /[oO]/) { + @patterns = map $self->{parent}->interpolate($_), @patterns + if $flags =~ /O/; + return unless length $data[0] or $flags =~ /z/; + for my $file (@patterns) { + if ($flags =~ /D/ and $file =~ m,(.*)[/\\],s) { + require File::Path; + File::Path::mkpath($1); + } + open OUT, "> $file" or die "open(`$file') for write: $!"; + if ($flags =~ /b/) { + binmode OUT; + } else { + my $e; + if ($e = $self->get_config('encode_encoding_files') and $e->[0]) { + eval "binmode OUT, ':encoding($e->[0])'"; # old binmode won't compile... + } + } + local ($/, $,) = ('', ''); + print OUT $data[0]; + close OUT or die "close(`$file') for write: $!"; + } + return; + } + if ($flags =~ /R/) { + @patterns = map $self->{parent}->parse_rex_prepare($_), @patterns; + } else { + @patterns = map $self->{parent}->parse_prepare($_), @patterns; + } + for $data (@data) { + my $pattern; + for $pattern (@patterns) { + last if $res = $self->{parent}->parse_rex_match($pattern, $data); + } + last if $res; + } + { local $" = "' `"; + die "Pattern(s) `@opatterns' did not succeed vs `@data'" + if $flags =~ /m/ and not $res; + } + my $k; + for $k (keys %$res) { + unless ($flags =~ /b/) { + $res->{$k} =~ s/^\s+//; + $res->{$k} =~ s/\s+$//; + } + delete $res->{$k} unless length $res->{$k} or $flags =~ /z/; + } + return unless $res and keys %$res; + return $res; +} + +# XXX Two decisions: which entries can access results of which ones, +# and which entries overwrite which ones; the user can reverse one of them +# by sorting config('parse_data') in the opposite order; but not both. +# Only practice can show whether our choice is correct... How to customize? + +sub parse { # Later recipies can access results of earlier ones. + my ($self,$what) = @_; + + return $self->{parsed}->{$what} # Recalculate during recursive calls + if not $self->{parsing} and exists $self->{parsed}; # Do not recalc after finish + + my $data = $self->get_config('parse_data'); + return unless $data and @$data; + my $parsing = $self->{parsing}; + local $self->{parsing}; + + my (%res, $d, $c); + for $d (@$data) { + $c++; + $self->{parsing} = $c; + # Protect against recursion: later $d can access results of earlier ones + last if $parsing and $parsing <= $c; + my $res = $self->parse_one($d); + # warn "Failure: [@$d]\n" unless $res; + # Set user-scratch space data immediately + for my $k (keys %$res) { + if ($k eq 'year') { # Do nothing + } elsif ($k =~ /^U(\d{1,2})$/) { + $self->{parent}->set_user($1, delete $res->{$k}) + } elsif (0 and $k =~ /^\w{4}(\d{2,})?$/) { + if (length $res->{$k} + or $self->get_config('id3v2_frame_empty_ok')->[0]) { + $self->{parent}->set_id3v2_frame($k, delete $res->{$k}) + } else { + delete $res->{$k}; + $self->{parent}->set_id3v2_frame($k); # delete + } + } elsif ($k =~ /^\w{4}(\d{2,}|(?:\(([^()]*(?:\([^()]+\)[^()]*)*)\))?(?:\[(\\.|[^]\\]*)\])?)$/) { + my $r = delete $res->{$k}; + $r = undef unless length $r or $self->get_config('id3v2_frame_empty_ok')->[0]; + if (defined $r or $self->{parent}->_get_tag('ID3v2')) { + $self->{parent}->select_id3v2_frame_by_descr($k, $r); + } + } + } + # later ones overwrite earlier + %res = (%res, %$res) if $res; + } + $self->{parsed} = \%res; + # return unless keys %res; + return $self->{parsed}->{$what}; +} + +for my $elt ( qw( title track artist album comment year genre ) ) { + no strict 'refs'; + *$elt = sub (;$) { + my $self = shift; + $self->parse($elt, @_); + } +} + +1; diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields.pm b/fhem/FHEM/lib/Normalize/Text/Music_Fields.pm new file mode 100644 index 000000000..b042b2e1a --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields.pm @@ -0,0 +1,1217 @@ +package Normalize::Text::Music_Fields; # Music_Normalize_Fields +$VERSION = '0.02'; +use strict; +use Config; +#use utf8; # Needed for 5.005... + +my %tr; +my %short; + +sub translate_dots ($) { + my $a = shift; + $a =~ s/^\s+//; + $a =~ s/\s+$//; + $a =~ s/\s+/ /g; + $a =~ s/\b(\w)\.\s*/$1 /g; + $a =~ s/(\w\.)\s*/$1 /g; + lc $a +} + +sub translate_tr ($) { + my $a = shift; + $a = $tr{translate_dots $a} or return; + return $a; +} + +sub strip_years ($) { # strip dates + my ($a) = (shift); + my @rest; + return $a unless $a =~ s/\s+((?:\([-\d,]+\)(\s+|$))+)$//; + @rest = split /\s+/, $1; + return $a, @rest; +} + +sub strip_duplicate_dates { # Remove $d[0] if it matches $d_r + my ($d_r, @d) = @_; + return unless @d; + $d_r = substr $d_r, 1, length($d_r) - 2; # Parens + my $dd = substr $d[0], 1, length($d[0]) - 2; # Parens + my @dates_r = split /,|--|-(?=\d\d\d\d)/, $d_r; + my @dates = split /,|--|-(?=\d\d\d\d)/, $dd; + for my $d (@dates) { + return @d unless grep /^\Q$d\E(-|$)/, @dates_r; + } + return @d[1..$#d]; +} + +sub __split_person ($) { + # Non-conflicting ANDs (0x438 is cyrillic "i", word is cyrillic "per") + split /([,;:]\s+(?:\x{043f}\x{0435}\x{0440}\.\s+)?|\s+(?:[-&\x{0438}ei]|and|et)\s+|\x00)/, shift; +} + +sub _translate_person ($$$); +sub _translate_person ($$$) { + my ($self, $aa, $with_year) = (shift, shift, shift); + my $fail = ($with_year & 2); + $with_year &= 1; + my $ini_a = $aa; + $aa = $aa->[0] if ref $aa; # [value, handler] + $aa =~ s/\s+$//; + load_lists() unless %tr; + # Try early fixing: + my $a1 = translate_tr $aa; + return ref $ini_a ? [$a1, $ini_a->[1]] : $a1 if $a1 and $with_year; + my ($a, @date) = strip_years($aa); + my $tr_a = translate_tr $a; + if (not defined $tr_a and $a =~ /(.*?)\s*,\s*(.*)/s) { # Schumann, Robert + $tr_a = translate_tr "$2 $1"; + } + if (not defined $tr_a) { + return if $fail; + my $ini = $aa; + # Normalize "translated" to "transl." + # echo "¯¥à¥¢®¤" | perl -wnle 'BEGIN{binmode STDIN, q(encoding(cp866))}printf qq(\\x{%04x}), ord $_ for split //' + $aa =~ s/(\s\x{043f}\x{0435}\x{0440})\x{0435}\x{0432}\x{043e}\x{0434}\x{0435}?(\s)/$1.$2/g; + $aa =~ s/(\s+)\x{0432}\s+(?=\x{043f}\x{0435}\x{0440}\.)/;$1/g; # v per. ==> , per. + $aa =~ s/[,;.]\s+(\x{043f}\x{0435}\x{0440}\.)\s*/; $1 /g; # normalize space, punct + $aa =~ s/\b(transl)ated\b/$1./g; + + my @parts = __split_person $aa; + if (@parts <= 1) { # At least normalize spacing: + # Add dots after initials + $aa =~ s/\b(\w)\s+(?=(\w))/ + ($1 ne lc $1 and $2 ne lc $2) ? "$1." : "$1 " /eg; + # Separate initials by spaces unless in a group of initials + $aa =~ s/\b(\w\.)(?!$|[-\s]|\w\.)/$1 /g; + return ref $ini_a ? [$aa, $ini_a->[1]] : $aa; + } + for my $i (0..$#parts) { + next if $i % 2; # Separator + my $val = _translate_person($self, $parts[$i], $with_year | 2); # fail + # Deal with cases (currently, in Russian only, after "transl.") + if (not defined $val and $i + and $parts[$i-1] =~ /^;\s+\x{043f}\x{0435}\x{0440}\.\s+$/ # per + and $parts[$i] =~ /(.*)\x{0430}$/s) { + $val = _translate_person($self, "$1", $with_year | 2); # fail + } + $val ||= _translate_person($self, $parts[$i], $with_year); # cosmetic too + $parts[$i] = $val if defined $val; + } + $tr_a = join '', @parts; + return $ini_a if $tr_a eq $ini; + @date = (); # Already taken into account... + } + my ($short, @date_r) = strip_years($tr_a); # Real date + @date = strip_duplicate_dates($date_r[0], @date) if @date_r == 1 and @date; + $tr_a = $short unless $with_year; + $a = join ' ', $tr_a, @date; + return ref $ini_a ? [$a, $ini_a->[1]] : $a; +} + +sub normalize_person ($$) { + return _translate_person(shift, shift, 1); +} + +for my $field (qw(artist artist_collection)) { + no strict 'refs'; + *{"normalize_$field"} = \&normalize_person; +} + +sub short_person ($$); +sub short_person ($$) { + my ($self, $a) = (shift, shift); + my $ini_a = $a; + $a = $a->[0] if ref $a; # [value, handler] + $a = _translate_person($self, $a, 0); # Normalize, no dates of life + $a =~ s/\s+$//; + ($a, my @date) = strip_years($a); + my @parts; + if (exists $short{$a}) { + $a = $short{$a}; + } elsif (@parts = __split_person $a and @parts > 1) { + for my $i (0..$#parts) { + next if $i % 2; # Separator + $parts[$i] = short_person($self, $parts[$i]); + } + $a = join '', @parts; + } else { + # Drop years of life + shift @date if @date and $date[0] =~ /^\(\d{4}-[-\d,]*\d{4,}[-\d,]*\)$/; + # Add dots after initials + $a =~ s/\b(\w)\s+(?=(\w))/ + ($1 ne lc $1 and $2 ne lc $2) ? "$1." : "$1 " /eg; + # Separate initials by spaces unless in a group of initials + $a =~ s/\b(\w\.)(?!$|[-\s]|\w\.)/$1 /g; + my @a = split /\s+/, $a; + # Skip shorting if there are strange non upcased parts (e.g., "-") or '()') + my @check = @a; + my $von = (@a > 2 and $a[-2] =~ /^[a-z]+$/); + splice @check, $#a - 1, 1 if $von; + # Ignore mid parts (skip if there are non upcased parts (e.g., "-") or '()') + unless (grep lc eq $_, @check or @a <= 1 or $a =~ /\(|[,;]\s/) { + my $i = substr($a[0], 0, 1); + $a[0] = "$i." if $a[0] =~ /^\w\w/ and lc($i) ne $i; + # Keep "from" in L. van Beethoven, M. di Falla, I. von Held, J. du Pre + @a = @a[0,($von ? -2 : ()),-1]; + } + $a = join ' ', @a; + } + $a = join ' ', $a, @date; + return ref $ini_a ? [$a, $ini_a->[1]] : $a; +} + +my %comp; + +sub normalize_file_lines ($$) { # Normalizing speeds up load_composer() + my ($self, $fn) = @_; + open my $f, '<', $fn or die "Can't open file $fn for read"; + local $_; + print "# normalized\n"; + while (<$f>) { + next if /^#\s*normalized\s*$/; + chomp; + $_ = normalize_piece($self, $_) unless /^\s*#/; + print "$_\n"; + } + close $f or die "Can't close file $fn for read"; +} + +sub _significant ($$$) { # Try to extract "actual name" of the piece + my ($tbl, $l, $r) = (shift, shift, shift); + my ($pre, $opus); + if ($tbl->{no_opus_no}) { # Remove year-like comment + ($pre) = ($l =~ /^(.*\S)\s*\(\d{4}\b[^()]*\)$/s); + } else { + ($pre, $opus) = ($l =~ /$r/); + } + $pre = $l unless $pre; + my ($significant) = ($pre =~ /^(.*?\bNo[.]?\s*\d+)/is); # Up to No. NN + ($significant) = ($pre =~ /^(.*?);/s) unless $significant; + ($significant) = $pre unless $significant; + (lc $significant, $opus); +} + +my $def_opus_rx = qr/\b(?:Op(?:us\b|\.)|WoO)\s*\d+[a-d]?(?:[.,;\s]\s*No\.\s*\d+(?:\.\d+)*)?/; + +sub _read_composer_file ($$*$$) { + my($self, $f, $fh, $tbl, $aka) = (shift,shift,shift,shift,shift); + my($normalized, $l, @works, %aka, $opened); + my $opus_rx = $tbl->{opus_rx} || $def_opus_rx; + my $opus_pref = $tbl->{opus_prefix} || 'Op.'; + local $/ = "\n"; # allow customization + if (defined $fh) { + $f |= "composer's file" . (eval {' for ' . $self->name_for_field_normalization} || ''); + } else { + open COMP, "< $f" or die "Can't read $f: $!"; + $fh = \*COMP; + $f = "`$f'"; + $opened = 1; + } + while (defined ($l = <$fh>)) { + next if $l =~ /^\s*(?:##|$)/; + if ($l =~ /^#\s*normalized\s*$/) { + $normalized++; # Very significant optimization (unless mail-header) + } elsif ($l =~ /^#\s*opus_rex\s(.*?)\s*$/) { + $opus_rx = $tbl->{opus_rx} = qr/$1/; + } elsif ($l =~ /^#\s*dup_opus_rex\s(.*?)\s*$/) { + $tbl->{dup_opus_rx} = qr/$1/; + } elsif ($l =~ /^#\s*opus_prefix\s(.*?)\s*$/) { + $opus_pref = $tbl->{opus_prefix} = $1; + } elsif ($l =~ /^#\s*no_opus_no\s*$/) { + $tbl->{no_opus_no} = 1; + } elsif ($l =~ /^#\s*opus_dup\s+(.*?)\s*$/) { + $tbl->{dup_opus}{lc $1} = 1; + } elsif ($l =~ /^#\s*prev_aka\s+(.*?)\s*$/) { + $aka->{$1} = $works[-1]; # recognize also alternative names + } elsif ($l =~ /^#\s*format\s*=\s*(line|mail-header)\s*$/) { + $/ = ($1 eq 'line' ? "\n" : ''); + } elsif ($l =~ /^#[^#]/) { + warn "Unrecognized line of $f: $l" + } elsif ($l !~ /^##/) { # Recursive call to ourselves... + if ($normalized) { + $l =~ s/\s*$//; # chomp... + } elsif ($/) { + $l = normalize_piece($self, $l); + } else { + $l = normalize_piece_mail_header($self, $l, $opus_rx, $opus_pref); + } + push @works, $l; + } + } + not $opened or close $fh or die "Error reading $f: $!"; + @works; +} + +sub read_composer_file ($$;*) { + my($self, $f, $fh) = (shift,shift,shift); + $self = prepare_tag_object_comp($self) unless ref $self; + _read_composer_file($self, $f, $fh,{},{}); +} + +my @path; +@path = ("$ENV{HOME}/.music_fields") + if defined $ENV{HOME} and -d "$ENV{HOME}/.music_fields"; +push @path, '-'; +@path = split /\Q$Config{path_sep}/, $ENV{MUSIC_FIELDS_PATH} + if defined $ENV{MUSIC_FIELDS_PATH}; + +sub set_path { + @path = @_; +} + +(my $myself = __PACKAGE__) =~ s,::,/,g; # 'Normalize/Text/Music_Fields.pm' +my @f = $INC{"$myself.pm"}; +warn("panic: can't find myself"), @f = () unless -r $f[0]; +s(\.pm$)()i or (@f=(), warn "panic: misformed myself") for @f; + +sub get_path () { + map +($_ eq '-' ? @f : $_), @path; +} + +sub load_composer ($$) { + my ($self, $c) = @_; + eval {$c = $self->shorten_person($c)}; + my $ini = $c; + return $comp{$ini} if exists $comp{$ini}; + $c =~ s/[^-\w]/_/g; + $c =~ s/__/_/g; + # XXX See Wikipedia "Opus number" for more complete logic + $comp{$ini}{opus_rx} = $def_opus_rx; + $comp{$ini}{opus_prefix} = 'Op.'; + my @dirs = get_path(); + my @files = grep -r $_, map "$_/$c.comp", @dirs or return 0; + my $f = $files[0]; +# $f = $c =~ tr( ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ\x80-\x9F) +# ( !cLXY|S"Ca<__R~o+23'mP.,1o>...?AAAAAAACEEEEIIIIDNOOOOOx0UUUUYpbaaaaaaaceeeeiiiidnooooo:ouuuuyPy_) +# unless -r $f; + #warn "file looked up is $f"; + return $comp{$ini} unless -r $f; + my $tbl = $comp{$ini}; + my ($normalized); + my @works = _read_composer_file($self, $f, undef, $tbl, \my %aka); + return unless @works; + # Piano Trio No. 8 (Arrangement of the Septet; Op. 20)); Op. 38 (1820--1823) + # so can't m/.*?/ + my $r = qr/^(.*($tbl->{opus_rx}))/s; + # Name "as in Wikipedia:Naming conventions (pieces of music)" + my (%opus, %name, %dup, %dupop); + for my $l (@works) { + my ($significant, $opus) = _significant($tbl, $l, $r); + if ($significant and $name{$significant}) { + $dup{$significant}++; + warn "Duplicate name `$significant': <$l> <$name{$significant}>" + if $ENV{MUSIC_DEBUG_TABLE}; + } + $name{$significant} = $l if $significant; + $opus or next; + $opus = lc $opus; + if ($opus{$opus}) { + $dupop{$opus}++; + warn "Duplicate opus number `$opus': <$l> <$opus{$opus}>" + unless $tbl->{dup_opus_rx} and $opus =~ /$tbl->{dup_opus_rx}/ + or $tbl->{dup_opus}{$opus}; + } + $opus{$opus} = $l; + } + delete $name{$_} for keys %dup; + delete $opus{$_} for keys %dupop; + for my $s (keys %aka) { + my ($n) = _significant($tbl, $s, $r); + warn "Duplicate and/or unnecessary A.K.A. name `$s' for <$aka{$s}>" + if $name{$n}; + $name{$n} = $aka{$s}; + $name{"\0$s"} = "\0$n"; # put into values(), see normalize_person() + } + $tbl->{works} = \@works; + $tbl->{opus} = \%opus if %opus; + $tbl->{name} = \%name if %name; + $tbl; +} + +sub translate_signature ($$$$) { # One should be able to override this + shift; + join '', @_; +} +$Normalize::Text::Music_Fields::translate_signature = \&translate_signature; + +my %alteration = (dur => 'major', moll => 'minor'); +my %mod = (is => 'sharp', es => 'flat', s => 'flat', # since Es means Ees + '#' => 'sharp', b => 'flat'); + +# XXXX German ==> English (nontrivial): H ==> B, His ==> B sharp, B ==> B flat +# XXXX Do not touch B (??? Check "Klavier" etc to detect German???) +my %key = (H => 'B'); + +sub normalize_signature ($$$$) { + my ($self, $key, $mod, $alteration) = @_; + $alteration ||= ($key =~ /[A-Z]/) ? ' major' : ' minor'; + $alteration = lc $alteration; + $alteration =~ s/^-?\s*/ /; + $alteration =~ s/(\w+)/ $alteration{$1} || $1 /e; + $mod =~ s/^-?\s*/ / if $mod; # E-flat, Cb + $mod = lc $mod; + $mod =~ s/(\w+|#)/ $mod{$1} || $1 /e; + $key = uc $key; + $key = $key{$key} || $key; + &$Normalize::Text::Music_Fields::translate_signature($self,$key,$mod,$alteration); +} + +my $post_opus_rex = qr/(?:[\-\/](?=\d)|(?:[,;.]?|\s)\s*(?:\bN(?:[or]|(?=\d))\.?|#|\x{2116}\.?))\s*(?=\d)/; + +sub normalize_opus ($$$) { + my ($self, $op, $no) = (shift, shift, shift); + my $have_no = ( $op =~ s/\b(?:[,;.]?|\s)\s*(?=No\.\s*\d+)/, / ); + $no = '' unless defined $no; + # nr12 n12 12 -12 #12 Numero_Sign 12 - but only if $op has no number already! + $no =~ s/^$post_opus_rex/, No. / unless $have_no; + # Now the tricky part: normalize the stuff in unknown format; + # XXXX Now support only "B. NNN" stuff + $op =~ s/^(\w)(\b|(?=\d))\.?\s*/\U$1. /; + "$op$no" +} + +# 1: prefix ("in" etc.), 2: letter, 3: modifier ("b" etc), 4: alteration: minor etc. +my $signature_rex = qr/(\s*(?:\bin\b|[,;.:]|^|\((?:in\s+)?(?=[-a-zA-Z#\s]+\)))\s*)([a-h])(\s*[b#]|(?:\s+|-)(?:flat|sharp)|[ie]s|(?<=e)s|)((?:(?:\s+|-)(?:major|minor|dur|moll))?)\)?(?=\s*[-;":]|$)/i; + +# All these should match in +# mp3info2 -D -a beethoven -t "# 28" "" +# (should give the same results): "wind in C" "tattoo" "WoO 20" +# "sonata in F#" "piano in F#" "op78" "Op. 10-2" "Op. 10, #2" "sonata #22" "WoO 205-1" + +sub find_person ($) { + my $self = shift; + eval {$self->name_for_field_normalization} || eval {$self->composer} + || $self->artist; +} + +# See test_normalize_piece() +sub _normalize_piece ($$$$) { + my ($self, $n, $improve_opus, $by_opus) = (shift, shift, shift, shift); + my $ini_n = $n; + $n = $n->[0] if ref $n; # [value, handler] + return $ini_n unless $n; + $n =~ s/^\s+//; + $n =~ s/\s+$//; + return $ini_n unless $n; + $n =~ s/\s{2,}/ /g; + + # Opus numbers + $n =~ s/\bOp(us\s+(?=\d)|[.\s]\s*|\.?(?=\d))/Op. /gi; # XXXX posth.??? + $n =~ s/\bN(?:[or]|(?=\d))\.?\s*(?=\d)/No. /gi; # nr12 n12 + $n =~ s/(?<!\w)[#\x{2116}]\s*(?=\d)/No. /gi; # #12, Numero Sign 12 + + my $c = find_person $self; + my $tbl = ($c and load_composer($self, $c)) || {}; + my $opus_rx = $tbl->{opus_rx} || $def_opus_rx; + + # XXXX Is this `?' for good? + $n =~ s/(?<=[^(.,;\s])(\s*[.,;])?\s*\b(?=$opus_rx)/; /gi + if $improve_opus; # punctuation before Op. + + # punctuation between Op. and No (as in Wikipedia for most expanded listings) + # $n =~ s/\b((Op\.|WoO)\s+\d+[a-d]?)(?:[,;.]?|\s)\s*(?=No\.\s*\d+)/$1, /gi; + $n =~ s/($opus_rx)($post_opus_rex\d+)?/ normalize_opus($self, $1, $2) /gie; + + # Tricky part: normalize "In b#"; allow just b# after punctuation too + $n =~ s/$signature_rex/ + ((not $1 or 'i' eq substr($1,0,1)) ? '' : ' ') . "in " + . normalize_signature($self,"$2","$3","$4")/ie; + my $canon; + { + $tbl or last; + # Convert Op. 23-3 to Op. and No +# my ($o, $no) = ($n =~ /\b(Op\.\s+\d+[a-d]?[-\/]\d+[a-d]?)((?:[,;.]?|\s)\s*(?:No\.\s*\d+))?/); +# $n =~ s/\b(Op\.\s+\d+[a-d]?)[-\/](\d+[a-d]?)/$1, No. $2/i +# if $o and not $no and $o !~ /^$opus_rx$/; + $tbl->{works} or last; + # XXX See Wikipedia "Opus number" for more complete logic + my ($opus) = ($n =~ /^.*($opus_rx)/); # at the end (one not in comments!) + if ($opus and $by_opus) { + $canon = $tbl->{opus}{lc $opus} or last; + } else { # $significant: Up to the first "No. NNN.N", or to the first ";" + my ($significant, $pre, $no, $post) = + ($n =~ /^((.*?)\bNo\b[.]?\s*(\d+(?:\.\d+)*))\s*(.*)/is); + ($significant) = ($n =~ /^(.*?);/s) unless $significant; + $significant ||= $n; + $canon = $tbl->{name}{lc $significant}; # Try exact match + if (not $canon) { # Try harder: match word-for-word + my ($ton, $rx_pre, $rx_post) = ('') x 3; + my $nn = $n; + if ($nn =~ s/\b(in\s+[A-H](?:\s+(?:flat|sharp))?\s+(?:minor|major))\b//) { + $ton = $1; + ($significant, $pre, $no, $post) = # Redo with $nn + ($nn =~ /^((.*?)\bNo\b[.]?\s*(\d+(?:\.\d+)*))\s*(.*)/is); + ($significant) = ($nn =~ /^(.*?);/s) unless $significant; + $significant ||= $nn; + $ton = '.*\b' . (quotemeta $ton) . '\b'; + } + $pre = $significant unless defined $pre; # Same with No removed + # my @parts2 = split '\W+', $post; + if ($pre and $pre =~ /\w/) { + $rx_pre = '\b' . join('\b.*\b', split /\W+/, $pre) . '\b'; + } + if ($post and $post =~ /\w/) { + $rx_post = '.*' . join '\b.*\b', split /\W+/, $post; + } + # warn "<$no> <$n> <$nn> <$ton> <$rx_pre> <$rx_post>"; + $no = '.*\bNo\.\s*' . (quotemeta $no) . '\b(?!\.\d)' if $no; + $no = '' unless defined $no; + last unless "$rx_pre$no$ton$rx_post"; + my $sep = $tbl->{no_opus_no} ? '' : '.*;'; + my $rx = qr/$rx_pre$no$ton$rx_post$sep/is; + my @matches = grep /$rx/, values %{$tbl->{name}}; + if (@matches == 1) { + $canon = $matches[0]; + } elsif (!@matches) { + last; + } else { # Many matches; maybe the shortest is substr of the rest? + my ($l, $s, $diff) = 1e100; + $l > length and ($s = $_, $l = length) for @matches; + $s eq substr $_, 0, $l or ($diff = 1, last) for @matches; + last if $diff; + $canon = $s; + } + $canon = $tbl->{name}{$canon} if $canon =~ s/^\0//s; # short name + } + } +# if ($canon) { +# my (%w, %w1); +# for my $w (split /[-.,;\s]+/, $canon) { +# $w{lc $w}++; +# } +# for my $w (split /[-.,;\s]+/, $n) { +# $w1{lc $w}++ unless $w{lc $w}; +# } +# if (%w1) { +# warn "Unknown words in title: `", join("` '", sort keys %w1), "'" +# unless $ENV{MUSIC_TRANSLATE_FIELDS_SKIP_WARNINGS}; +# last +# } +# } + $n = $canon; # XXXX Simple try (need to compare word-for-word) + } + return ref $ini_n ? [$n, $ini_n->[1]] : $n; +} + +sub normalize_piece ($$) { + _normalize_piece(shift, shift, 'improve opus', 'by opus'); +} + +sub opus_parser ($) { + my $tag = shift; + my $c = find_person $tag; + my $tbl = ($c and load_composer($tag, $c)); + my $opus_rx = $tbl->{opus_rx} || $def_opus_rx; + my $opus_pre = $tbl->{opus_prefix} || 'Op.'; + ($opus_rx, $opus_pre, $c) +} + +sub full_opus ($$;$$) { + my ($tag, $short, $opus_rx, $opus_pref) = (shift, shift, shift, shift); + ($opus_rx, $opus_pref) = opus_parser($tag) unless $opus_rx; + + $short = "$opus_pref $short" if $short =~ /^\d/ and not $short =~ /$opus_rx/; + $short =~ s/^($opus_rx)($post_opus_rex\d+)?/ normalize_opus($tag, $1, $2) /gie; + $short +} + +# Currently used Title-* fields: RAW, Opus, Dates, Key, Name, Related-Name, +# Alternative-Name, Punct, Type, Count, For, Type-After-Name, In-Movements +# Related-On, Comment, Related-After, Name-By-First-Row +## [When new added, change also the "merging" logic in merge_info().] +sub normalize_mail_header_line ($$;$$) { + my ($tag, $in, $opus_rx, $opus_pref) = (shift, shift, shift, shift); + my ($t, $v) = $in =~ /^([-\w]+):\s*(.*)$/s or die; + $v = "($v)" if $t eq 'Title-Dates'; + $v = full_opus $tag, $v, $opus_rx, $opus_pref + if $t eq 'Title-Opus' and $v =~ /(^\d|[\-\/])/; + $v = "; $v" if $t eq 'Title-Opus'; + $v = qq("$v") if $t =~ /^Title(-Related)?-Name$/; + $v = qq(["$v"]) if $t =~ /^Title-Name-By-First-Row$/; + $v = qq(; "$v") if $t eq 'Title-Alternative-Name'; + $v =~ s/^(in\s+)?/in /i if $t =~ 'Title-Key'; + $v = "No. $v" if $t eq 'Title-No'; + $v = "for $v" if $t eq 'Title-For'; + $v = "on $v" if $t eq 'Title-Related-On'; + $v = "(lyrics by $v)" if $t eq 'Title-Lyrics-By'; + $v = ", $v" if $t eq 'Title-Type-After-Name'; + $v; +} + +## perl -wple "BEGIN {print q(# format = mail-header)} s/#\s*normalized\s*$//; $_ = qq(Title: $_) unless /^\s*(#|$)/; $_ = qq(\n$_) if $p and not /^##/; $_ .= qq(\n) unless $p = /^##/" Normalize::Text::Music_Fields-G_Gershwin.comp >Music_Fields-G_Gershwin.comp-mail +sub normalize_piece_mail_header ($$;$$) { + my ($tag, $in, $opus_rx, $opus_pref) = (shift, shift, shift, shift); + return $1 if $in =~ /^Title:\s*(.*?)\s*$/m; + my @pieces = map normalize_mail_header_line($tag, $_, $opus_rx, $opus_pref), + grep /^Title-[-\w]+:\s/, split /\n/, $in; + for my $i (1 .. @pieces - 1) { + $pieces[$i-1] .= ' ' + unless $pieces[$i-1] =~ /[\(\[\{]$/ or $pieces[$i] =~ /^[\)\]\}.,;:?!]/; + } + return join '', @pieces; +} + +sub shorten_opus ($$$$) { # $mp3, $str, $pre + my ($tag, $op, $pref, $rx) = (shift, shift, shift, shift); + my ($out, $cut) = ($op, ''); + if ($out =~ s/^\Q$pref\E\s*(?=\d)//) { + if ($out =~ $rx) { # back up if shortened version causes confusion + $out = $op; + } else { + $cut = $pref; + } + } + my $out1 = $out; + if ($out =~ s/(\d[a-i]?),\s+No\.\s*(?=\d)/$1-/) { + my $o = full_opus($tag, $out, $rx, $pref); + if ($op ne $o or $out =~ /^$rx$/) { # check again + $out = $out1; + unless ($out eq $op) { # Extra sanity check + $o = full_opus($tag, $out, $rx, $pref); + $out = $op unless $op eq $o; + } + } + } + $out +} + +my $main_instr = join '|', qw(Piano Violin Viola Cello Horn String Wind Harp + Instrument Clarinet Alto); +my $for_instr = join '|', qw(Mandolin Harpsichord chorus soprano alt bass + basses tenor mezzo-soprano \(mezzo\)soprano baritone contralto hand + soli soloists woodwinds celesta accordion instrumentalists large small + double violoncello clarinet oboe english french bassoon trombone organ + flute voice orchestra military band chamber symphonic symphony electric + percussion double-bass vibraphone pantomime instrumental ensemble tape + timpani bells keyboard guitar triple percussionist counter-tenor alto + counter-alto male female children's boys' mixed a capella cappella choir + basssoli chamberorchestra metronome triangle harmonium trumpet); +my $multiplets = join '|', qw(solo duo duet trio quartet quintet sextet septet octet); +my $pieces = join '|', qw(Serenada Serenade Romance Song Notturno Aria Mass + Allemande Chorus Allegretto Rondo Opera Fantasia Polonaise Contredanse + Prelude Andante Cadenza Bagatelle Cantata Aria Joke Waltz Waltzes Minuet + Ländler March Rondino Variations Equali Fugue Piece Symphony Sonata + Concerto Sonatina Dance Mignon Fantasy Scherzo Polka Moderato Fragment + Transcription Orchestration Suite Music Reduction Passacaglia Arrangement + accompaniment choral score Operetta Ballet oratorio Choruses Intermezzo + Overture Dialogue Epilogue Aphorism Monologue Gallop Interlude + Re-orchestration Reorchestration Cycle Potpourri Nocturne Capriccio + Mazurek Mazurka Impromptu Humoresque Ballade Ballads Gavotte Requiem + Fanfares Motet Rhapsodies Rhapsody Intermezzi Poem Marches Theme + Melody); + +my $numb_rx = qr/one|two|three|four|five|six|seven|eight|nine/i; + +my $count_rx = qr/ \d+ + | (?:$numb_rx)(?:teen)? + | ten|eleven|twelve|thirteen|fifteen|eighteen + | (?:twenty|thirty|fourty|fifty|sixty|seventy|eighty|ninety) + (?: (?:\s+ | -) (?:$numb_rx) )? /ix; + +#no utf8; # `use' is needed by 5.005 + +my $for_rx = qr/ (?:\s+|^) + for + (?: (?:\s+|(?<=\/)) \(? + (?:and|or|&|vocal\s+soloist|$main_instr|$for_instr|prepared\s+piano|magnetic\s+tape|stage\s+orchestra|jazz\s+ensemble|(?:vocal\s+)?(?:$multiplets)|$count_rx|[23456789]|[12345]\d|Große Fuge) + (?:s|\(s\))? \)? + [,\/]? + )+ + /ix; + +my $piece_rx = qr/ (?: (?:Transcription|Orchestration|Reduction|Arrangement|Suite|Instrumentation|Re-?orchestration) + \s+ of + (?: \s+ (?: $main_instr | the | $count_rx ) )? + \s+ )? # Mod + (?: + (?: $main_instr | Vocal | secular | sacred + | Double | Triple | Easy | Trio | Symphonic ) + \s+ )? # Prefix + (?:Concerto\s+grosso | $multiplets + | Ecossaise? + | (?:[123456]-part\s+)? (?:riddle\s+)? Canon + | (?:sets\s+of\s+)? (?: chorale\s+preludes? | $pieces ) + (?: s? \s* (?:\band\b|&) \s* (?:$pieces))? + | Incidental\s+music | electronic\s+composition + | chorale\s+prelude + | Musical\s+greetings? | choral\s+score | vocal\s+quartet + | (?:heroic|comic|tragic|historical)\s+opera + | scenic\s+composition | symphonic\s+poem ) # Main type + (?: s? \s+ in \s+ (?:$numb_rx) \s+ act )? + /ix; + +#use utf8; # needed by 5.005 + +my $name_rx = qr/ (?: [A-Z]\w* \.? \s+)* [A-Z][-\'\w]+ /x; + +my $rel_piece_rx = # Two Pieces for Erwin Dressel's Opera "Armer Columbus" + qr/ \b + (?:to|from|of|a\s+fter|for|on(?:\s+motives\s+of)?) + (?: + \s+ (?: \s+ music \s+ to)? (?: the | $name_rx\'s ) # Erwin Dressel's + (?: \s+ (?: (?:(?:silent|animated)\s+)? film | spectacle | comedy + | TV[-\s]+production | music\s+to\s+the\s+film + | play | (?:Chamber-?\s*)? opera | stage \s+ revue | novel))?)? \b + /ix; + + +sub strip_known_from_end ($$$) { + my ($tag, $in, $try_key, @tail) = (shift, shift, shift); + # E.g., when the second name is based on the first line of lyrics: + unshift @tail, "Title-Lyrics-By: $1" if $in =~ s/\s+\(lyrics\s+by\s+([^()]+)\)$//; + unshift @tail, "Title-Alternative-Name: $4" + while $in =~ s/^(.*?".*?".*)(\s*[.:,;])?\s+(?(2)|(?=\())(\()?"([^\"]+)"(?(3)\)|)$/$1/; + + # Too much recognized as this if ??? + while ( $in =~ s/ \s* ( $rel_piece_rx | (?!$) [.:,;]? ) + (?: \s+ + ( (\[)? ["\x{201E}]([^\"\x{201C}\x{201E}]+)["\x{201C}] (?(3) \] | ) + | \(["\x{201E}]([^\"\x{201C}\x{201E}]+)["\x{201C}]\) )) $ + //xo ) { + if (length $1 <= 1) { + unshift @tail, "Title-Name: $+"; + } else { + unshift @tail, "Title-Related-Name: $+" if $2; + unshift @tail, "Title-Related-How: $1"; + } + } + unshift @tail, "Title-Related-By: after $1" + if $in =~ s/ \s* after \s+ ($name_rx) $//xo; + + unshift @tail, "Title-Related-On: $+" # Variation and Fugue + if $in =~ s/ ( \b variations? (?: \s+ and \s+ $piece_rx)? (?:$for_rx)? ) + \s+ on \s+ # on a Hungarian melody + (an? \s+ (?: (?: $name_rx | original ) \s+)? $piece_rx + (?: \s+ by \s+ $name_rx)? )$/$1/xio; # XXXX Why $+ needed? + + unshift @tail, "Title-In-Movements: $1" + if $in =~ s/\s*(in\s+(a\s+single|$numb_rx|\d)\s+(movement|episode)s?)$//; + + unshift @tail, "Title-Key: " . normalize_signature($tag, "$2", "$3", "$4") + if $in =~ s/\s*$signature_rex$//; + if ($in =~ s/\s*([.,;:])?\s+No\.\s*(\d+[a-d]?(\.\d+)?)$//i) { + unshift @tail, "Title-No: $2"; + unshift @tail, "Title-Punct: $1" if $1; + } + + unshift @tail, "Title-Key: " . normalize_signature($tag, "$2", "$3", "$4") + if $try_key and $in =~ s/[:;,]?\s*$signature_rex$//; + + my $f; + ($f = $1) =~ s/^\s*for\s*//, unshift @tail, "Title-For: $f" + if $in =~ s/($for_rx)$//io; # XXXX: foo arranged for piano ??? + + if ($in =~ s/\s*([.,;:])?\s+No.\s*(\d+[a-d]?(\.\d+)?)$//i) { # Repeat + unshift @tail, "Title-No: $2"; + unshift @tail, "Title-Punct: $1" if $1; + } + + ($in, @tail); +} + +sub parse_piece ($$$$$$$); # Predeclaration for recursive call without () +sub parse_piece ($$$$$$$) { + my ($after_name, $at_end, $at_start, $tag, $in, $opus_pref, $opus_rx, @tail) + = (shift, shift, shift, shift, shift, shift, shift); + if ($at_end) { + unshift @tail, "Title-Dates: $2" + if $in =~ s/(.*\S)\s*\(([^()]*\b\d{4}\b[^()]*)\)$/$1/ # $1 makes greedy + or $at_end and not $at_start and + $in =~ s/^()\s*\(([^()]*\b\d{4}\b[^()]*)\)$/$1/; # $1 makes greedy + unshift @tail, "Title-Opus: " . shorten_opus($tag, "$2", $opus_pref, $opus_rx) + while $in =~ s/(.*);\s+($opus_rx)\s*$/$1/; + unshift @tail, "Title-Key: " . normalize_signature($tag, "$2", "$3", "$4") + if $in =~ s/\s*$signature_rex$//; + } + ($in, my @r) = strip_known_from_end($tag, $in, 'look for key'); + unshift @tail, @r; + + # Now recognize comment as everything after a key (except, maybe, name) + if ($in =~ /^(.*\S)\s*$signature_rex\s*(?:"([^\"]+)"\s*)?(?:([.,:;])\s)?(.*)$/) { + $in = $1; + my $k = normalize_signature($tag, "$3", "$4", "$5"); + my($n,$rest) = ($6, $8); + if (length $rest) {{ # Localize match + unshift @tail, + 'Title-'. ($8 =~ /^[^\s\w]$/ ? 'Punct' : 'Comment'). ": $rest"; + }} + unshift @tail, "Title-Punct: $7" if $7; + my $alt = ($in =~ /".*"/ ? '-Alternative' : ''); + unshift @tail, "Title$alt-Name: $n" if defined $n and length $n; + unshift @tail, "Title-Key: $k"; + } + + # Now repeat looking for known fields + ($in, @r) = strip_known_from_end($tag, $in, not 'look for key'); + unshift @tail, @r; + + if ($at_start) { # and (@tail or not $at_end) + unshift @tail, "Title-Type: $1" if $in =~ s/^($piece_rx s?)\s*$//iox; + unshift @tail, "Title-Count: $1" , "Title-Type: $2" + if $in =~ s/^($count_rx)\s+( $piece_rx s?)\s*$//iox; + unshift @tail, "Title-Count: $1" + if $in =~ s/^($count_rx)\s*$//iox; + } + if (not @tail and $at_start and $at_end) { + unshift @tail, "Title: $in"; + } elsif (not length $in) { # Do nothing + } elsif ($in =~ /^\s*[-,:;.()\[\]{}]\s*$/) { + unshift @tail, "Title-Punct: $in"; + } elsif ($after_name and $in =~ /^(by|after)((\s+and)?\s+[A-Z][-\'\w]+)+\s*$/) { + unshift @tail, "Title-Related-By: $in"; + } elsif ($after_name and $in =~ /^([-,;:])\s+($piece_rx s?)\s*$/iox) { + unshift @tail, "Title-Type-After-Name: $2"; + } elsif ($at_start and $in =~ /^"([^\"]+)"\s*$/iox) { + unshift @tail, "Title-Name: $1"; + } else { + if ($at_start and $in =~ /^"([^\"]+)"[,.;:]\s*(\S.*?)\s*$/) { + my $name = $1; # Pretend we are at start: + my @rest = parse_piece 'after_name', ($at_end and not @tail), 'start', + $tag, "$2", $opus_pref, $opus_rx; + unshift @rest, "Title-Punct: ," + unless $rest[0] =~ s/^Title-Type:/Title-Type-After-Name:/; + return("Title-Name: $name", @rest, @tail) + unless (join "\n", '', @rest) =~ /\nTitle-RAW:/; + } + unshift @tail, "Title-RAW: $in"; + } + @tail; +} + +my %html_esc = qw( amp & lt < gt > ); + +sub naive_format ($$$) { # Used to find glaring errors in conversion only + my ($tag, $in, $opus_rx, $opus, @out) = (shift,shift,shift); + $in =~ s/^($opus_rx)\n/$1: /; + my @in = split /\s*\n\s*/, $in; + if ($in[0] =~ s/^($opus_rx)[:,]\s*/Title-RAW: /) { + ($opus = $1) =~ s/^Opus\b/Op./; + } + for my $l (@in) { + if ($l =~ s/^Title-Bold:\s*//) { + push @out, qq("$l"); + } elsif ($l =~ s/^Title-Opus:\s*//) { + push @out, '; ' . full_opus $tag, "$l"; + } elsif ($l =~ s/^Title-Dates:\s*//) { + push @out, "($l)"; + } elsif ($l =~ s/^X-\w[-\w]*:\s*//) { # Do nothing + } elsif ($l =~ s/^Title-(RAW|Comment):\s*//) { + push @out, $l if length $l; + } else { + warn "Naive formatting: Unknown line format `$l'" + } + } + if (defined $opus) { + my @year; + @year = $1 if @out and $out[-1] =~ s/\s*(\([^()]*\b\d{4}\b[^()]*\))$//; + pop @out unless @out and length $out[-1]; + push @out, "; $opus", @year; + } + for my $n (1..$#out) { + $out[$n] =~ s/^(?![.,;:])/ /; + } + join '', @out +} + +# Convert from line-format to mail-header format: +## perl -MNormalize::Text::Music_Fields -wlne "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print q(# format = mail-header)} print Normalize::Text::Music_Fields::emit_as_mail_header($tag,$_, 0,$pre)" gershwin Music_Fields-G_Gershwin.comp-line >Music_Fields-G_Gershwin.comp-mail1 +# (inverse transformation:) Dump pieces listed in mail-header format +## perl -MNormalize::Text::Music_Fields -wle "print for Normalize::Text::Music_Fields::read_composer_file(shift, shift)" gershwin Music_Fields-G_Gershwin.comp-mail > o +# +## perl -MNormalize::Text::Music_Fields -00wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print q(# format = mail-header)} print Normalize::Text::Music_Fields::emit_as_mail_header($tag,$_, q(bold,xml,opus),$pre)" shostakovich o-xslt-better >Music_Fields-D_Shostakovich.comp-mail1 +## perl -MNormalize::Text::Music_Fields -wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print qq(# format = mail-header\n)} print Normalize::Text::Music_Fields::emit_as_mail_header($tag,$_, q(opus), $pre)" schnittke o-schnittke-better >Music_Fields-A_Schnittke.comp-mail2 +sub emit_as_mail_header ($$$$) { # $mp3, $str, $has_bold_parts_etc, $pre [R/W] + my ($tag, $in, $preformatted) = (shift, shift, shift); + $in =~ s/#\s*normalized\s*$//; + #return "\n" if $in =~ /^\s*$/; + my @out; + unless ($in =~ /^\s*(#|$)/) { + return "\n\n" if $preformatted and $in =~ /^<\?xml\b/; + my $ini = my $ini_raw = $in; + $in =~ s/&(amp|lt|gt);/$html_esc{$1}/g if $preformatted =~ /\bxml\b/; + $in =~ s/&#x([\da-f]+);/chr hex $1/gei if $preformatted =~ /\bxml\b/; + + my ($opus_rx, $opus_pre) = opus_parser($tag); + + my $have_op = ($in =~ /^$opus_rx:/); + # When $use_only_opus, all the text but Opus-No is ignored; bad for update + my $use_only_opus = ($preformatted =~ /\bonly_by_opus\b/); + $in = _normalize_piece($tag, $in, !$have_op, $use_only_opus) + unless $preformatted =~ /\bbold\b/; + + $ini = naive_format($tag, $in, $opus_rx) if $preformatted =~ /\b(opus|bold)\b/; + my @op; + my $prefix = ($preformatted =~ /\bbold\b/ ? 'Title-RAW: ' : ''); + if ($in =~ s/^($opus_rx)(?:[:,](?:[ \t]+|(?=\n))|\n\s*)/$prefix/) { + my $op = $1; + my $o_pre = $opus_pre; + $o_pre = 'Opus' if $op =~ /^Opus\b/; + @op = "Title-Opus: " . shorten_opus($tag, $op, $o_pre, $opus_rx); + } elsif ($preformatted =~ /\bopus\b/) { + warn "Expected to start with `Opus NUMBER: ': <<<$in>>>"; + } + if ($preformatted =~ /\bbold\b/) { + my @parts = split /\s*\n\s*/, $in; + my ($after_for, $after_name); + for my $n (0..$#parts) { + my $p = $parts[$n]; + $p =~ s/\s+$//; + if ($p =~ s/^Title-Bold:\s*//) { + my $rel = $after_for ? '-Related' : ''; + push @out, "Title$rel-Name: $p"; + $after_for = 0, $after_name = 1; + next; + } elsif ($p =~ /^Title-RAW:\s*$/) { # Do nothing + next; + } elsif ($after_for = + ($n != $#parts and $parts[$n+1] =~ /^Title-Bold:\s*/ + and $parts[$n] =~ /^Title-RAW:\s*/ + # Title-RAW: Two Pieces for Erwin Dressel's Opera "Armer Columbus" + and $p =~ s/ \s* ( $rel_piece_rx \s*$ )//ixo)) { + my $how = $1; + $p =~ s/^Title-RAW:\s+// + or warn "Expected to start with Title-RAW: <<<$p>>>"; + push @out, + parse_piece $after_name,!'end', !$n, $tag, $p, $opus_pre, $opus_rx; + push @out, "Title-Related-How: $how"; + } elsif ($p =~ s/^Title-Opus:\s+// ) { + push @out, 'Title-Opus: ' . full_opus $tag, $p, $opus_rx, $opus_pre; + $after_name = 0; + } elsif ($p =~ /^(Title-(Opus|Comment|Dates)|X-Title-Opus-Alt):\s+/ ) { # Keep intact + push @out, $p; + $after_name = 0; + } else { + $p =~ s/^Title-RAW:\s+// or warn "Expected to start with `Title-RAW: ': <<<$p>>>"; + push @out, parse_piece $after_name, $n==$#parts, !$n, $tag, $p, $opus_pre, $opus_rx; + $after_name = 0; + } + } + } else { + @out = parse_piece 0, 'at_end', 'at_start', $tag, $in, $opus_pre, $opus_rx; + } + my @y; + unshift @y, pop @out while $out[-1] =~ /^Title-Dates:\s/; + push @out, @op, @y; + $out[0] =~ s/^Title:/Title-RAW:/ if @out > 1; # Opus 1: foo + $in = join "\n", @out, ($preformatted =~ /\bbold\b/ ? ('','') : ()); # \n\n + + my $res = normalize_piece_mail_header($tag, $in, $opus_rx, $opus_pre); + warn "# Mismatch:\n# in = $ini\n# out = $res\n#rawin= $ini_raw\n" unless $res eq $ini; + } + $in = "\n$in" if $in !~ /^\s*##/ and $_[0] and not $preformatted =~ /\bbold\b/; + $in .= qq(\n) unless $preformatted =~ /\bbold\b/ or $_[0] = ($in =~ /^##/); + $in; # Caller appends extra \n +} + +## perl -MNormalize::Text::Music_Fields -wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print qq(# format = mail-header\n)} next unless s/^\s*\+\+\s*//; print Normalize::Text::Music_Fields::merge_info($tag,$_, q(opus))" brahms o-brahms-op-no1-xslt +sub merge_info ($$$;$$) { # $update not fully implemented + my ($tag, $in, $preformatted, $soft, $update) = (shift, shift, shift, shift, shift); + my $parsed = emit_as_mail_header($tag, $in, $preformatted, my $pre); + my $op_n = ($parsed =~ /^Title-Opus: (.*)/m and $1); + die "Can't find opus number in `$in'" unless defined $op_n; + my $op_no = full_opus $tag, $op_n; + + $parsed =~ s/^Title-Punct:\s*-\nTitle-Name:/Title-Name-By-First-Row:/; + $soft ||= qr(^(?!)); # Never match + warn "Opus [$op_n]: Type `$1' interpreted as Title-Name\n" + if $op_n =~ $soft and $parsed =~ s/^Title-Type:/Title-Name:/m + and $parsed =~ /^Title-Name:\s*(.*)/; + warn("Too many fields in `$parsed', skipping"), return '' + if $parsed =~ /^(?=.)(?!Title-(?:Opus|RAW|Name(?:-By-First-Row)?|Key|Dates):)/m; + + my $name = normalize_piece $tag, $op_no; # expand opus+no to the full name + + if ($name eq $op_no) { # No current information + my ($opus_rx, $opus_pre) = opus_parser($tag); + die "No subopus number in `$op_no' (from `$in')" + unless $op_no =~ /^($opus_rx)\s*[.,:;]\s*No/; + my $op = $1; + $name = normalize_piece $tag, $op; # Expands opus to the full name + $update = 0; + } elsif (not $update) { + die "Opus `$op_no' already known: `$name'"; + } + + my $parsed_op = emit_as_mail_header($tag, $name, 'only_by_opus', my $pre1); + warn("Prior knowledge not found for `$in'\n"), + return $parsed if $parsed_op =~ /^Title:/; # Not found, or not parsable + + unless ($update) { # Handling "a group name" + $parsed_op =~ s/^Title-Count:.*\n//; # Four ballades for piano + if ($parsed_op =~ /^Title-Type:\s*(.*)\n/) { # Strip the plural + my $type = $1; + $type =~ s/^ Sets \s+ of \s+/Set of /x + or $type =~ s/^ ($piece_rx) (?:s | es) $/$1/x; # Strip the plural + $parsed_op =~ s/^.*/Title-Type: $type/; + } + $parsed_op =~ s/^Title-Opus:.*/Title-Opus: $op_n/m + or die "Can't find Opus: `$parsed_op'"; + } + if ($parsed =~ /^Title-Dates:\s*(.*)/m) { + my $d = $1; # (?<!.) does as /^/m, but matches at end too + $parsed_op =~ s/(?<!.)(Title-Dates:.*\n|\Z)/Title-Dates: $d\n/ or die; + } + if ($parsed =~ /^Title-Key:\s*(.*)/m) { + my $k = $1; + die "Key mismatch: $k vs $1" + if $parsed_op =~ /^Title-Key:\s*(.*)/m and $1 ne $k; + # XXXX Where put the key? STD orders: Type/No/Key/For or Type/For/No/Key + # There is also (beeth) Type/For/Related-On/Key??? Type/For/Key??? + $parsed_op =~ s/(?<!.)(?=Title-(?!(?:Type|For|Related-On|No):)|\Z)/Title-Key: $k\n/ or die; + } + if ($parsed =~ s/^Title-RAW:/Title-Name:/m) { + (my $n) = ($parsed =~ /^Title-Name:\s*(.*)/m); + warn "Title-RAW `$n' interpreted as Title-Name in `$in'\n"; + } + if ($parsed =~ /^(Title-Name(?:[-\w]*):\s*.*)/m) { # pre: Type-After-Name, In-Movements + my $n = $1; # Related-On, Comment, Related-After + $parsed_op =~ s/(?<!.)(?=Title-(?:Type-After-Name|In-Movements|Related-On|Comment|Related-After|Opus|Dates):|\Z)/$n\n/ or die; + } + $parsed_op +} + +for my $field (qw(album title title_track)) { + no strict 'refs'; + *{"normalize_$field"} = \&normalize_piece; +} + +# perl -Ii:/zax/bin -MNormalize::Text::Music_Fields -wle "BEGIN{binmode $_, ':encoding(cp866)' for \*STDIN, \*STDOUT, \*STDERR}print Normalize::Text::Music_Fields->check_persons" +sub check_persons ($) { + my $self = shift; + my %seen; + $seen{$_}++ for values %tr; + for my $l (keys %seen) { + my $s = short_person($self, $l); + my $ll = normalize_person($self, $s); + warn "`$l' => `$s' => `$ll'" unless $ll eq $l; + } + %seen = (); + $seen{$_}++ for values %short; + for my $s (values %seen) { + my $l = normalize_person($self, $s); + my $ss = short_person($self, $l); + warn "`$s' => `$l' => `$ss'" unless $ss eq $s; + } +} + +my %aliases; + +sub load_lists () { + my @dirs = get_path(); + my @lists = map <$_/*.lst>, @dirs; + #warn "dirs=`@dirs', lists=`@lists'\n"; + warn("panic: can't find name lists in `@dirs'"), return 0 unless @lists; + + for my $f (@lists) { + local $/ = "\n"; + open F, "< $f" or warn("Can't open `$f' for read: $!"), next; + my @in = <F>; + close F or warn("Can't close `$f' for read: $!"), next; + my $charset; + for (@in) { + next if /^\s*$/; + if ( /^ \s* \# \s* (?:charset|encoding) \s* = \s* ("?) (.*?) \1 \s* $/ix) { + $charset = $2; + require Encode; + next; + } + $_ = Encode::decode($charset, $_) if $charset; # Make empty to disable + s/^\s+//, s/\s+$//, s/\s+/ /g; + next if /^##/; + if (/^ \# \s* (alias|fix|shortname_for) \s+ (.*?) \s* => \s* (.*)/x) { + if ($1 eq 'alias') { + $aliases{$2} = [split /\s*,\s*/, $3]; + } elsif ($1 eq 'fix') { + my ($old, $ok) = ($2, $3); + $tr{translate_dots $old} = $tr{translate_dots $ok} || $ok; + #print "translating `",translate_dots $old,"' to `",translate_dots $ok,"'\n"; + } elsif ($1 eq 'shortname_for') { + my ($long, $short) = ($2, $3); + $tr{translate_dots $short} = $long; + ($long) = strip_years($long); + $short{$long} = $short; + } + next; + } + if (/^ \# \s* fix_firstname \s+ (.*\s(\S+))$/x) { + $tr{translate_dots $1} = $tr{translate_dots $2}; + next; + } + if (/^ \# \s* keep \s+ (.*?) \s* $/x) { + $tr{translate_dots $1} = $1; + next; + } + if (/^ \# \s* shortname \s+ (.*?) \s* $/x) { + my $in = $1; + my $full = __PACKAGE__->_translate_person($in, 0); + unless (defined $full and $full ne $in) { + my @parts = split /\s+/, $in; + $full = __PACKAGE__->_translate_person($parts[-1], 0); + warn("Can't find translation for `@parts'"), next + unless defined $full and $full ne $parts[-1]; + # Add the normalization + my $f = __PACKAGE__->normalize_person($parts[-1]); + $tr{translate_dots $in} = $f; + } + $short{$full} = $in; + ($full) = strip_years($full); + $short{$full} = $in; + next; + } + warn("Do not understand directive: `$_'"), next if /^#/; + #warn "Doing `$_'"; + my ($pre, $post) = /^(.*?)\s*(\(.*\))?$/; + my @f = split ' ', $pre or warn("`$pre' won't split"), die; + my $last = pop @f; + my @last = $last; + + # no utf8; # `use' is needed by 5.005 + (my $ascii = $last) =~ + tr( ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ\x80-\x9F) + ( !cLXY|S"Ca<__R~o+23'mP.,1o>...?AAAAAAACEEEEIIIIDNOOOOOx0UUUUYpbaaaaaaaceeeeiiiidnooooo:ouuuuyPy_); + push @last, $ascii unless $ascii eq $last; + my $a = $aliases{$last[0]} ? $aliases{$last[0]} : []; + $a = [$a] unless ref $a; + push @last, @$a; + for my $last (@last) { + my @comp = (@f, $last); + $tr{"\L@comp"} ||= $_; + $tr{lc $last} ||= $_; # Two Bach's + if (@f) { + $tr{"\L$f[0] $last"} ||= $_; # With the first of pre-names only + my @ini = map substr($_, 0, 1), @f; + $tr{"\L$ini[0] $last"} ||= $_; # One initial + $tr{"\L@ini $last"} ||= $_; # All initials + } + } + } + } +} + +#$tr{lc 'Tchaikovsky, Piotyr Ilyich'} = $tr{lc 'Tchaikovsky'}; + +sub prepare_tag_object_comp ($;$) { + my ($comp, $piece) = @_; + require MP3::Tag; + my $tag = MP3::Tag->new_fake('settable'); + + for my $elt ( qw( title track artist album comment year genre + title_track artist_collection person ) ) { + no strict 'refs'; + MP3::Tag->config("translate_$elt", \&{"Normalize::Text::Music_Fields::normalize_$elt"}) + if defined &{"Normalize::Text::Music_Fields::normalize_$elt"}; + # This is needed to expand albums, since pieces file is named so... + MP3::Tag->config("short_person", \&Normalize::Text::Music_Fields::short_person) + if defined &Normalize::Text::Music_Fields::short_person; + } + $tag->config('parse_data', ['mi', $comp, '%a'], ($piece ? ['mi', $piece, '%l'] : () )); + $tag; +} + +## perl -MNormalize::Text::Music_Fields -e Normalize::Text::Music_Fields::test_normalize_piece +sub test_normalize_piece { + for (split /\n/, <<EOS) { +beethoven # 28 +beethoven wind in C +beethoven tattoo +beethoven WoO 20 +beethoven sonata in F# +beethoven piano in F# +beethoven op78 +beethoven Op. 10-2 +beethoven Op. 10, #2 +beethoven sonata #22 +beethoven WoO 205-1 +beethoven WoO 205, No 1 +beethoven WoO 205, No. 1 +beethoven WoO 205, no 1 +beethoven WoO 205;#1 +beethoven WoO 205, no1 +beethoven WoO 205 #1 +beethoven WoO 205#1 +beethoven WoO 205. #1 +- beethoven WoO 205,-1 +- beethoven WoO 205, -1 +- beethoven WoO 205 -1 +- beethoven WoO 205 1 +- beethoven WoO 205;1 +EOS + my $match = (s/^-\s*// ? '-' : '+'); + s/^(\w+)\s+//; + my $tag = prepare_tag_object_comp("$1", $_); + print "$match ", find_person($tag), " ", $tag->album, "\n"; + } +} + +for my $elt ( qw( title track artist album comment year genre + title_track artist_collection person ) ) { + no strict 'refs'; # backward compatibility layer: + *{"translate_$elt"} = \&{"normalize_$elt"} if defined &{"normalize_$elt"}; +} + +1; + +=head1 NAME + +Normalize::Text::Music_Fields - normalize names of people's and (musical) works. + +=head1 SYNOPSIS + + $name = $obj->Normalize::Text::Music_Fields::normalize_person($name); + $work = $obj->Normalize::Text::Music_Fields::normalize_piece($work); + # $obj should have methods `name_for_field_normalization', 'shorted_person' + +=head1 DESCRIPTION + +Databases of names and of works-per-name are taken from plain-text +files (optionally in mail-header format). Names are stored in F<*.lst> files. +Works are stored in F<.comp> files named after the shortened name +of the composer. + +The directories of these files are looked in the environment variable +C<MUSIC_FIELDS_PATH> (if defined, split the same way as C<PATH>), or in +C<$ENV{HOME}/.music_fields>, and C<-> (and C<-> is replaced by the directory +named as the module file with F<.pm> dropped). At runtime, one can +replace the list by calling function Normalize::Text::Music_Fields::set_path() +with the list of directories as the argument. + +(Since parsed files are cached, replacing the directory list should be done +as early as possible.) + +Files may be managed with utility subroutines provided with the module: + + # Translate from one-per-line to mail-header format: + perl -wple "BEGIN {print q(# format = mail-header)} s/#\s*normalized\s*$//; $_ = qq(Title: $_) unless /^\s*(#|$)/; $_ = qq(\n$_) if $p and not /^##/; $_ .= qq(\n) unless $p = /^##/" Normalize::Text::Music_Fields-G_Gershwin.comp >Music_Fields-G_Gershwin.comp-mail + + # (inverse transformation:) Dump pieces listed in mail-header format + perl -MNormalize::Text::Music_Fields -wle "print for Normalize::Text::Music_Fields::read_composer_file(shift, shift)" gershwin Music_Fields-G_Gershwin.comp-mail > o + + # Normalize data in 1-line-per piece format + perl -MNormalize::Text::Music_Fields -wle "Normalize::Text::Music_Fields::prepare_tag_object_comp(shift)->Normalize::Text::Music_Fields::normalize_file_lines(shift)" + + # Create a mail-header file from a semi-processed (with "bold" fields) + # mail-header file (with xml escapes, preceded by opus number) + perl -MNormalize::Text::Music_Fields -00wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print q(# format = mail-header)} print Normalize::Text::Music_Fields::emit_as_mail_header($tag,$_, q(bold,xml,opus),$pre)" shostakovich o-xslt-better >Music_Fields-D_Shostakovich.comp-mail1 + + # Likewise, from work-per-line with opus-numbers: + perl -MNormalize::Text::Music_Fields -wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print qq(# format = mail-header\n)} print Normalize::Text::Music_Fields::emit_as_mail_header($tag,$_, q(opus), $pre)" schnittke o-schnittke-better >Music_Fields-A_Schnittke.comp-mail2 + + # A primitive tool for merging additional info into the database: + perl -MNormalize::Text::Music_Fields -wnle "BEGIN {$tag = Normalize::Text::Music_Fields::prepare_tag_object_comp(shift @ARGV); print qq(# format = mail-header\n)} next unless s/^\s*\+\+\s*//; print Normalize::Text::Music_Fields::merge_info($tag,$_, q(opus,xml), qr(^(58|70|76|116|118|119)($|-)))" brahms o-brahms-op-no1-xslt + + # Minimal consistency check of persons database. + perl -MNormalize::Text::Music_Fields -wle "BEGIN{binmode $_, ':encoding(cp866)' for \*STDIN, \*STDOUT, \*STDERR} print Normalize::Text::Music_Fields->check_persons" + + # Minimal testing code: + perl -MNormalize::Text::Music_Fields -e Normalize::Text::Music_Fields::test_normalize_piece + +It may be easier to type these examples if one uses C<manage_M_N_F.pm>, which +exports the mentioned subroutines to the main namespace (available in +F<examples> directory of a distribution of C<MP3::Tag>). E.g., the last +example becomes: + + perl -Mmanage_M_N_F -e test_normalize_piece + + +=cut + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Dvor_k.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Dvor_k.comp new file mode 100644 index 000000000..767237126 --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Dvor_k.comp @@ -0,0 +1,1861 @@ +# format = mail-header + + +# opus_rex \b(?:[Bb](?:\b|(?=\d))\.?|Op(?:us\b|\.))\s*\d+[a-d]?(?:[.,;\s]\s*No\.\s*\d+(?:\.\d+)*)? + +# opus_dup Op. 11 + +# opus_dup Op. 112 + +# opus_dup Op. 114 + +# opus_dup Op. 115 + +# opus_dup Op. 12 + +# opus_dup Op. 14 + +# opus_dup Op. 19a + +# opus_dup Op. 19b + +# opus_dup Op. 25 + +# opus_dup Op. 30 + +# opus_dup Op. 32 + +# opus_dup Op. 37 + +# opus_dup Op. 40 + +# opus_dup Op. 46 + +# opus_dup Op. 49 + +# opus_dup Op. 50 + +# opus_dup Op. 53 + +# opus_dup Op. 54 + +# opus_dup Op. 59 + +# opus_dup Op. 62 + +# opus_dup Op. 64 + +# opus_dup Op. 68, No. 5 + +# opus_dup Op. 71 + +# opus_dup Op. 72 + +# opus_dup Op. 79 + +# opus_dup Op. 8 + +# opus_dup Op. 84 + +# opus_dup Op. 86 + +# opus_dup Op. 94 + +# opus_dup Op. 99 + + + + + + +Title-Name: Forget-me-not Polka +Title-For: piano +Title-Opus: B. 1 +Title-Dates: 1856 + + + +Title-Type: Mass +Title-Key: B flat major +Title-Comment: [lost] +Title-Opus: B. 2 +Title-Dates: 1859 + + + +Title-Type: Polka +Title-Key: E major +Title-For: piano +Title-Opus: B. 3 +Title-Dates: 1860 + + + +Title-Name: The Woman Harpist +Title-Comment: [lost] +Title-Opus: B. 4 +Title-Dates: 1860 + + + +Title-Type: Polka +Title-Comment: [lost] +Title-Opus: B. 5 +Title-Dates: 1860 + + + +Title-Type: Gallop +Title-Comment: [lost] +Title-Opus: B. 6 +Title-Dates: 1860 + + + +Title-Type: String Quintet +Title-No: 1 +Title-Key: A minor +X-Title-Opus-Alt: B. 7 +Title-Opus: Op. 1 +Title-Dates: 1861 + + + +Title-Type: String Quartet +Title-No: 1 +Title-Key: A major +X-Title-Opus-Alt: B. 8 +Title-Opus: Op. 2 +Title-Dates: 1862 + + + +Title-Type: Symphony +Title-No: 1 +Title-Key: C minor +Title-Name: The Bells of Zlonice +Title-Opus: B. 9 +Title-Dates: 1865 + + + +Title-Type: Cello Concerto +Title-Key: A major +Title-Comment: [orchestrated by Jarmil Burghauser] +Title-Opus: B. 10 +Title-Dates: 1865 + + + +Title-Name: Cypresses +Title-Comment: [18 songs on poems by G. Pfleger-Moravsky] +Title-Opus: B. 11 +Title-Dates: 1865 + + + +Title-Type: Symphony +Title-No: 2 +Title-Key: B flat major +X-Title-Opus-Alt: B. 12 +Title-Opus: Op. 4 +Title-Dates: 1865 + + + +Title-Count: Two +Title-Type: Songs +Title-For: baritone +Title-Opus: B. 13 +Title-Dates: 1865 + + + +Title-Type: Clarinet Quintet +Title-Key: B flat minor +Title-Comment: [lost] +Title-Opus: B. 14 +Title-Dates: 1866 + + + +Title-Count: Seven +Title-Type: Interludes +Title-For: orchestra +Title-Opus: B. 15 +Title-Dates: 1867 + + + +Title-Type: Serenade +Title-For: flute, violin, viola, and triangle +Title-Opus: B. 15bis +Title-Dates: 1867 + + + +Title-Name: Alfred +Title-RAW: , heroic opera in 3 acts +Title-Opus: B. 16 +Title-Dates: 1870 + + + +Title-RAW: Tragic Overture +Title-Related-How: from +Title-Related-Name: Alfred +Title-Opus: B. 16a +Title-Dates: 1870 + + + +Title-Type: String Quartet +Title-No: 2 +Title-Key: B flat major +Title-Opus: B. 17 +Title-Dates: 1869 + + + +Title-Type: String Quartet +Title-No: 3 +Title-Key: D major +Title-Opus: B. 18 +Title-Dates: 1870 + + + +Title-Type: String Quartet +Title-No: 4 +Title-Key: E minor +Title-Opus: B. 19 +Title-Dates: 1870 + + + +Title-Type: Cello Sonata +Title-Key: F minor +Title-Comment: [lost] +Title-Opus: B. 20 +Title-Dates: 1871 + + + +Title-Name: The King and the Charcoalburner +Title-Type-After-Name: comic opera +Title-Comment: [1st version] +X-Title-Opus-Alt: B. 21 +Title-Opus: Op. 12 +Title-Dates: 1871 + + + +Title-RAW: Concert Overture +Title-Key: F major +Title-Comment: [from 1st version of "The King and the Charcoalburner"] +X-Title-Opus-Alt: B. 21a +Title-Opus: Op. 12 +Title-Dates: 1871 + + + +Title-Type: Potpourri +Title-Related-How: from +Title-Related-Name: The King and the Charcoalburner I +Title-Opus: B. 22 +Title-Dates: 1872 + + + +Title-RAW: Songs on words by Eliska Krásnohorská +Title-Opus: B. 23 +Title-Dates: 1871 + + + +Title-Name: Sirotek the Orphan +Title-Comment: [after a ballad by K. J. Erben] +Title-Opus: B. 24 +Title-Dates: 1871 + + + +Title-Name: Rosemary +Title-Comment: [after a poem by K. J. Erben] +Title-Opus: B. 24a +Title-Dates: 1871 + + + +Title-Type: Piano Trio +Title-Comment: [lost] +X-Title-Opus-Alt: B. 25 +Title-Opus: Op. 13, No. 1 +Title-Dates: 1871 + + + +Title-Type: Piano Trio +Title-Comment: [lost] +X-Title-Opus-Alt: B. 26 +Title-Opus: Op. 13, No. 2 +Title-Dates: 1872 + + + +Title-Name: The Heirs of the White Mountain +Title-Comment: [hymn after a poem by V. Halék] +Title-Opus: B. 27 +Title-Dates: 1872 + + + +Title-Type: Piano Quintet +Title-No: 1 +Title-Key: A major +X-Title-Opus-Alt: B. 28 +Title-Opus: Op. 5 +Title-Dates: 1872 + + + +Title-RAW: Four Songs on Serbian Folk Poems +X-Title-Opus-Alt: B. 29 +Title-Opus: Op. 6 +Title-Dates: 1872 + + + +Title-Type: Songs +Title-Related-How: from the +Title-Related-Name: Dvur Králove +Title-Punct: ( +Title-Name: Queen's Court +Title-RAW: ) Manuscript +X-Title-Opus-Alt: B. 30 +Title-Opus: Op. 7 +Title-Dates: 1872 + + + +Title-Count: Three +Title-Type: Nocturnes +Title-Comment: [incomplete, except for No. 2 "May Night"] +Title-Opus: B. 31 +Title-Dates: 1872 + + + +Title-Name: Silhouettes +Title-For: piano +X-Title-Opus-Alt: B. 32 +Title-Opus: Op. 8 +Title-Dates: 1872 + + + +Title-Type: Violin Sonata +Title-Key: A minor +Title-Comment: [lost] +Title-Opus: B. 33 +Title-Dates: 1873 + + + +Title-Type: Symphony +Title-No: 3 +Title-Key: E flat major +X-Title-Opus-Alt: B. 34 +Title-Opus: Op. 10 +Title-Dates: 1873 + + + +Title-Name: Romeo and Juliet +Title-RAW: Overture +Title-Comment: [lost] +Title-Opus: B. 35 +Title-Dates: 1873 + + + +Title-RAW: Octet Serenade +Title-Comment: [lost] +Title-Opus: B. 36 +Title-Dates: 1873 + + + +Title-Type: String Quartet +Title-No: 5 +Title-Key: F minor +X-Title-Opus-Alt: B. 37 +Title-Opus: Op. 9 +Title-Dates: 1873 + + + +Title-Type: Romance +Title-Key: F minor +Title-For: violin and piano +X-Title-Opus-Alt: B. 38 +Title-Opus: Op. 11 +Title-Dates: 1877 + + + +Title-Type: Romance +Title-Key: F minor +Title-For: violin and orchestra +Title-Comment: [arrangement of B. 38] +X-Title-Opus-Alt: B. 39 +Title-Opus: Op. 11 +Title-Dates: 1877 + + + +Title-Type: String Quartet +Title-No: 6 +Title-Key: A minor +X-Title-Opus-Alt: B. 40 +Title-Opus: Op. 12 +Title-Dates: 1873 + + + +Title-RAW: Andante appassionato +Title-Key: A minor +Title-For: string quartet +Title-Opus: B. 40a +Title-Dates: 1873 + + + +Title-Type: Symphony +Title-No: 4 +Title-Key: D minor +X-Title-Opus-Alt: B. 41 +Title-Opus: Op. 13 +Title-Dates: 1874 + + + +Title-Name: The King and the Charcoalbuner +Title-Comment: [2nd version of B. 21] +X-Title-Opus-Alt: B. 42 +Title-Opus: Op. 14 +Title-Dates: 1874 + + + +Title-RAW: Porpourri on The King and the Charcoalburner +Title-Comment: [2nd version of B. 22] +Title-Opus: B. 43 +Title-Dates: 1875 + + + +Title-RAW: Symphonic Poem (Rhapsody) +Title-Key: A minor +X-Title-Opus-Alt: B. 44 +Title-Opus: Op. 14 +Title-Dates: 1874 + + + +Title-Type: String Quartet +Title-No: 7 +Title-Key: A minor +X-Title-Opus-Alt: B. 45 +Title-Opus: Op. 16 +Title-Dates: 1874 + + + +Title-Name: The Stubborn Lovers +Title-Type-After-Name: comic opera +X-Title-Opus-Alt: B. 46 +Title-Opus: Op. 17 +Title-Dates: 1874 + + + +Title-Type: Nocturne +Title-Key: B major +Title-For: strings +X-Title-Opus-Alt: B. 47 +Title-Opus: Op. 40 +Title-Dates: 1883 + + + +Title-Type: Nocturne +Title-Key: B major +Title-For: violin and piano +X-Title-Opus-Alt: B. 48a +Title-Opus: Op. 40 +Title-Dates: 1883 + + + +Title-Type: Nocturne +Title-Key: B major +Title-For: piano four hands +X-Title-Opus-Alt: B. 48b +Title-Opus: Op. 40 +Title-Dates: 1875 + + + +Title-Type: String Quintet +Title-No: 2 +Title-Key: G major +Title-Comment: [formerly Op. 18] +X-Title-Opus-Alt: B. 49 +Title-Opus: Op. 77 +Title-Dates: 1875 + + + +Title-RAW: Moravian Duets +X-Title-Opus-Alt: B. 50 +Title-Opus: Op. 20 +Title-Dates: 1875 + + + +Title-Type: Piano Trio +Title-No: 1 +Title-Key: B flat major +X-Title-Opus-Alt: B. 51 +Title-Opus: Op. 21 +Title-Dates: 1875 + + + +Title-Type: Serenade +Title-For: Strings +Title-Key: E major +X-Title-Opus-Alt: B. 52 +Title-Opus: Op. 22 +Title-Dates: 1875 + + + +Title-Type: Piano Quartet +Title-No: 1 +Title-Key: D major +X-Title-Opus-Alt: B. 53 +Title-Opus: Op. 23 +Title-Dates: 1875 + + + +Title-Type: Symphony +Title-No: 5 +Title-Key: F major +Title-Comment: [formerly Op. 24] +X-Title-Opus-Alt: B. 54 +Title-Opus: Op. 76 +Title-Dates: 1875 + + + +Title-Name: Vanda +Title-Type-After-Name: tragic opera +X-Title-Opus-Alt: B. 55 +Title-Opus: Op. 25 +Title-Dates: 1875 + + + +Title-Type: Piano Trio +Title-No: 2 +Title-Key: G minor +X-Title-Opus-Alt: B. 56 +Title-Opus: Op. 26 +Title-Dates: 1876 + + + +Title-Type: String Quartet +Title-No: 8 +Title-Key: E major +Title-Comment: [formerly Op. 27] +X-Title-Opus-Alt: B. 57 +Title-Opus: Op. 80 +Title-Dates: 1876 + + + +Title-Count: Two +Title-Type: Minuets +Title-For: piano +X-Title-Opus-Alt: B. 58 +Title-Opus: Op. 28 +Title-Dates: 1876 + + + +Title-Count: Four +Title-Type: Choruses +Title-For: mixed choir +X-Title-Opus-Alt: B. 59 +Title-Opus: Op. 29 +Title-Dates: 1876 + + + +Title-RAW: Moravian Duets +X-Title-Opus-Alt: B. 60 +Title-Opus: Op. 32 +Title-Dates: 1876 + + + +Title-Name: Evening Songs +Title-Comment: [after poems by V. Hálek] +X-Title-Opus-Alt: B. 61 +Title-Opus: Op. 31 +Title-Dates: 1876 + + + +Title-RAW: Moravian Duets +X-Title-Opus-Alt: B. 62 +Title-Opus: Op. 32 +Title-Dates: 1876 + + + +Title-Type: Piano Concerto +Title-Key: G minor +X-Title-Opus-Alt: B. 63 +Title-Opus: Op. 33 +Title-Dates: 1876 + + + +Title-RAW: Dumka +Title-Key: D minor +Title-For: piano +X-Title-Opus-Alt: B. 64 +Title-Opus: Op. 35 +Title-Dates: 1876 + + + +Title-RAW: Theme and Variations +Title-For: piano +X-Title-Opus-Alt: B. 65 +Title-Opus: Op. 36 +Title-Dates: 1876 + + + +Title-RAW: Choral Songs +Title-For: male voices +Title-Opus: B. 66 +Title-Dates: 1877 + + + +Title-Name: The Cunning Peasant +Title-Type-After-Name: comic opera +X-Title-Opus-Alt: B. 67 +Title-Opus: Op. 37 +Title-Dates: 1877 + + + +Title-Type: Overture +Title-Related-How: to +Title-Related-Name: The Cunning Peasant +X-Title-Opus-Alt: B. 67a +Title-Opus: Op. 37 +Title-Dates: 1877 + + + +Title-RAW: Ave Maria +X-Title-Opus-Alt: B. 68 +Title-Opus: Op. 19b +Title-Dates: 1877 + + + +Title-RAW: Moravian Duets +X-Title-Opus-Alt: B. 69 +Title-Opus: Op. 38 +Title-Dates: 1877 + + + +Title-Type: Symphonic Variations +Title-Comment: [formerly Op. 28] +X-Title-Opus-Alt: B. 70 +Title-Opus: Op. 78 +Title-Dates: 1877 + + + +Title-RAW: Stabat Mater +X-Title-Opus-Alt: B. 71 +Title-Opus: Op. 58 +Title-Dates: 1877 + + + +Title-Name: Bouquet of Czech Folksongs +Title-Opus: B. 72 +Title-Dates: 1877 + + + +Title-Name: Song of a Czech +Title-Opus: B. 73 +Title-Dates: 1877 + + + +Title-RAW: Scottish Dances +Title-For: piano +X-Title-Opus-Alt: B. 74 +Title-Opus: Op. 41 +Title-Dates: 1877 + + + +Title-Type: String Quartet +Title-No: 9 +Title-Key: D minor +Title-Comment: [dedicated to Johannes Brahms] +X-Title-Opus-Alt: B. 75 +Title-Opus: Op. 34 +Title-Dates: 1877 + + + +Title-Name: From a Bouquet of Czech Folksongs +X-Title-Opus-Alt: B. 76 +Title-Opus: Op. 43 +Title-Dates: 1878 + + + +Title-Type: Serenade +Title-Key: D minor +Title-For: Wind Instruments +X-Title-Opus-Alt: B. 77 +Title-Opus: Op. 44 +Title-Dates: 1878 + + + +Title-RAW: Slavonic Dances for piano four hands, 1st series +X-Title-Opus-Alt: B. 78 +Title-Opus: Op. 46 +Title-Dates: 1878 + + + +Title-Type: Bagatelles +Title-For: two violins, cello, and harmonium/piano +X-Title-Opus-Alt: B. 79 +Title-Opus: Op. 47 +Title-Dates: 1878 + + + +Title-Type: String Sextet +Title-Key: A major +X-Title-Opus-Alt: B. 80 +Title-Opus: Op. 48 +Title-Dates: 1878 + + + +Title-Type: Capriccio +Title-For: violin and piano +X-Title-Opus-Alt: B. 81 +Title-Opus: Op. 24 +Title-Dates: 1878 + + + +Title-Name: Hymnus ad Laudus in festo Sanctae Trinitatis +Title-Opus: B. 82 +Title-Dates: 1878 + + + +Title-RAW: Slavonic Dances +Title-For: orchestra +Title-Comment: [orchestration of B. 78] +X-Title-Opus-Alt: B. 83 +Title-Opus: Op. 46 +Title-Dates: 1878 + + + +Title-RAW: Three Modern Greek Songs +X-Title-Opus-Alt: B. 84a +Title-Opus: Op. 50 +Title-Dates: 1878 + + + +Title-RAW: Three Modern Greek Songs +X-Title-Opus-Alt: B. 84b +Title-Opus: Op. 50 +Title-Dates: 1878 + + + +Title-RAW: Furiants +Title-For: piano +X-Title-Opus-Alt: B. 85 +Title-Opus: Op. 42 +Title-Dates: 1878 + + + +Title-RAW: Slavonic Rhapsodies +X-Title-Opus-Alt: B. 86 +Title-Opus: Op. 45 +Title-Dates: 1878 + + + +Title-RAW: Choral Songs +Title-For: male voices +X-Title-Opus-Alt: B. 87 +Title-Opus: Op. 27 +Title-Dates: 1878 + + + +Title-RAW: Festival March +Title-For: orchestra +X-Title-Opus-Alt: B. 88 +Title-Opus: Op. 54 +Title-Dates: 1879 + + + +Title-Type: Mazurek +Title-For: violin and piano +X-Title-Opus-Alt: B. 89 +Title-Opus: Op. 49 +Title-Dates: 1879 + + + +Title-Type: Mazurek +Title-For: violin and orchestra +X-Title-Opus-Alt: B. 90 +Title-Opus: Op. 49 +Title-Dates: 1879 + + + +Title-RAW: Psalm 149 +Title-For: choir and orchestra +Title-Comment: [1st version] +X-Title-Opus-Alt: B. 91 +Title-Opus: Op. 79 +Title-Dates: 1879 + + + +Title-Type: String Quartet +Title-No: 10 +Title-Key: E flat major +X-Title-Opus-Alt: B. 92 +Title-Opus: Op. 51 +Title-Dates: 1879 + + + +Title-RAW: Czech Suite +Title-Key: D major +Title-For: orchestra +X-Title-Opus-Alt: B. 93 +Title-Opus: Op. 39 +Title-Dates: 1879 + + + +Title-Type: Polonaise +Title-Key: A major +Title-For: cello and piano +Title-Opus: B. 94 +Title-Dates: 1879 + + + +Title-Name: Ave maris stella +X-Title-Opus-Alt: B. 95a +Title-Opus: Op. 19b +Title-Dates: 1879 + + + +Title-Name: O sanctissima dulcis Virgo Maria! +Title-RAW: for alto, baritone, and organ +X-Title-Opus-Alt: B. 95b +Title-Opus: Op. 19a +Title-Dates: 1879 + + + +Title-Name: O sanctissima dulcis Virgo Maria +Title-Comment: [version of B. 95b, substituting soprano for baritone] +X-Title-Opus-Alt: B. 95b bis +Title-Opus: Op. 19a +Title-Dates: 1879 + + + +Title-Type: Violin Concerto +Title-Key: A minor +X-Title-Opus-Alt: B. 96 +Title-Opus: Op. 53 +Title-Dates: 1880 + + + +Title-Name: Vanda +Title-RAW: , concert overture +X-Title-Opus-Alt: B. 97 +Title-Opus: Op. 25 +Title-Dates: 1879 + + + +Title-Name: Silhouettes +Title-For: piano +X-Title-Opus-Alt: B. 98 +Title-Opus: Op. 8 +Title-Dates: 1879 + + + +Title-RAW: Prague Waltzes +Title-For: orchestra +Title-Opus: B. 99 +Title-Dates: 1879 + + + +Title-Type: Polonaise +Title-Key: E flat major +Title-For: orchestra +Title-Opus: B. 100 +Title-Dates: 1879 + + + +Title-Type: Waltzes +Title-For: piano +Title-Comment: [Orchestrated by Burghauser  ] +X-Title-Opus-Alt: B. 101 +Title-Opus: Op. 54 +Title-Dates: 1880 + + + +Title-Name: The Heirs of the White Mountain +Title-Comment: [after a poem by V. Halék] +X-Title-Opus-Alt: B. 102 +Title-Opus: Op. 30 +Title-Dates: 1880 + + + +Title-RAW: Eclogues +Title-For: piano +X-Title-Opus-Alt: B. 103 +Title-Opus: Op. (56) +Title-Dates: 1880 + + + +Title-RAW: Gypsy Songs +Title-Comment: [after poems by A. Heyduk] +X-Title-Opus-Alt: B. 104 +Title-Opus: Op. 55 +Title-Dates: 1880 + + + +Title-Count: Two +Title-Type: Waltzes +Title-For: strings +X-Title-Opus-Alt: B. 105 +Title-Opus: Op. 54 +Title-Dates: 1880 + + + +Title-Type: Violin Sonata +Title-Key: F major +X-Title-Opus-Alt: B. 106 +Title-Opus: Op. 57 +Title-Dates: 1880 + + + +Title-RAW: Moravian Duets +X-Title-Opus-Alt: B. 107 +Title-Opus: Op. 32 +Title-Dates: 1880 + + + +Title-Type: Violin Concerto +Title-Key: A minor +Title-Comment: [final version of B. 96] +X-Title-Opus-Alt: B. 108 +Title-Opus: Op. 53 +Title-Dates: 1880 + + + +Title-RAW: Album Leaves +Title-For: piano +Title-Opus: B. 109 +Title-Dates: 1880 + + + +Title-Type: Piano Pieces +X-Title-Opus-Alt: B. 110 +Title-Opus: Op. 52 +Title-Dates: 1880 + + + +Title-Type: Mazurkas +Title-For: piano +X-Title-Opus-Alt: B. 111 +Title-Opus: Op. 56 +Title-Dates: 1880 + + + +Title-Type: Symphony +Title-No: 6 +Title-Key: D major +X-Title-Opus-Alt: B. 112 +Title-Opus: Op. 60 +Title-Dates: 1879 + + + +Title-Name: Child's Song +Title-Opus: B. 113 +Title-Dates: 1880 + + + +Title-Type: Polka +Title-For: orchestra +Title-Name: For the Prague Students +X-Title-Opus-Alt: B. 114 +Title-Opus: Op. 53A/1 +Title-Dates: 1880 + + + +Title-Name: Ballad of King Matthias +Title-Comment: [from 2nd version of The King and the Charcoalburner, B. 42] +X-Title-Opus-Alt: B. 115 +Title-Opus: Op. 14 +Title-Dates: 1881 + + + +Title-Type: Moderato +Title-Key: A major +Title-For: piano +Title-Opus: B. 116 +Title-Dates: 1881 + + + +Title-Name: Legends +Title-For: piano four hands +X-Title-Opus-Alt: B. 117 +Title-Opus: Op. 59 +Title-Dates: 1881 + + + +Title-Name: There on our roof ... +Title-Comment: [Moravian folk song] +Title-Opus: B. 118 +Title-Dates: 1881 + + + +Title-Type: Gallop +Title-Key: E major +Title-For: orchestra +X-Title-Opus-Alt: B. 119 +Title-Opus: Op. 53A/2 +Title-Dates: 1881 + + + +Title-RAW: String Quartet Fragment +Title-Key: F major +Title-Opus: B. 120 +Title-Dates: 1881 + + + +Title-Type: String Quartet +Title-No: 11 +Title-Key: C major +X-Title-Opus-Alt: B. 121 +Title-Opus: Op. 61 +Title-Dates: 1881 + + + +Title-Name: Legends +Title-For: orchestra +Title-Comment: [orchestration of B. 117] +X-Title-Opus-Alt: B. 122 +Title-Opus: Op. 59 +Title-Dates: 1881 + + + +Title-RAW: Songs on Poems by Pfleger-Moravsky +Title-Opus: B. 123, No. 4 +Title-Dates: 1881 + + + +Title-Name: Josef Kajetán Tyl +Title-Comment: [incidental music for the play by F. F. Samberk] +X-Title-Opus-Alt: B. 125 +Title-Opus: Op. 62 +Title-Dates: 1881 + + + +Title-Name: My Home +Title-Type-After-Name: overture +Title-Related-How: to +Title-Related-Name: Josef Kajetán Tyl +X-Title-Opus-Alt: B. 125a +Title-Opus: Op. 62 +Title-Dates: 1882 + + + +Title-Name: In Nature's Realm +Title-Comment: [after poems by V. Hálek] +X-Title-Opus-Alt: B. 126 +Title-Opus: Op. 63 +Title-Dates: 1882 + + + +Title-Name: Dmitrij +Title-RAW: historical opera +X-Title-Opus-Alt: B. 127 +Title-Opus: Op. 64 +Title-Dates: 1882 + + + +Title-Name: Dmitrij +Title-Type-After-Name: overture +X-Title-Opus-Alt: B. 127a +Title-Opus: Op. 64 +Title-Dates: 1882 + + + +Title-RAW: Two Evening Songs +Title-Comment: [after poems by V. Hálek] +X-Title-Opus-Alt: B. 128 +Title-Opus: Op. 3 +Title-Dates: 1882 + + + +Title-Type: Impromptu +Title-Key: D minor +Title-For: piano +Title-Opus: B. 129 +Title-Dates: 1883 + + + +Title-Type: Piano Trio +Title-No: 3 +Title-Key: F minor +X-Title-Opus-Alt: B. 130 +Title-Opus: Op. 65 +Title-Dates: 1883 + + + +Title-RAW: Scherzo capriccioso +Title-For: orchestra +X-Title-Opus-Alt: B. 131 +Title-Opus: Op. 66 +Title-Dates: 1883 + + + +Title-Name: Hussite +Title-RAW: Overture +X-Title-Opus-Alt: B. 132 +Title-Opus: Op. 67 +Title-Dates: 1883 + + + +Title-Name: From the Bohemian Forest +Title-For: piano four hands +Title-Comment: [Orchestrated by Henk de Vlieger] +X-Title-Opus-Alt: B. 133 +Title-Opus: Op. 68 +Title-Dates: 1883 + + + +Title-Name: The Heirs of the White Mountain +Title-Comment: [revision of B. 102] +X-Title-Opus-Alt: B. 134 +Title-Opus: Op. 30 +Title-Dates: 1883 + + + +Title-Name: The Specter's Bride +Title-RAW: , dramatic cantata +X-Title-Opus-Alt: B. 135 +Title-Opus: Op. 69 +Title-Dates: 1884 + + + +Title-RAW: Dumka +Title-Key: C minor +Title-For: piano +X-Title-Opus-Alt: B. 136 +Title-Opus: Op. 12, No. 1 +Title-Dates: 1884 + + + +Title-RAW: Furiant +Title-Key: G minor +Title-For: piano +X-Title-Opus-Alt: B. 137 +Title-Opus: Op. 12, No. 2 +Title-Dates: 1879 + + + +Title-Type: Humoresque +Title-Key: F sharp major +Title-For: piano +Title-Opus: B. 138 +Title-Dates: 1884 + + + +Title-Type: Ballade +Title-Key: D minor +Title-For: piano +X-Title-Opus-Alt: B. 139 +Title-Opus: Op. 15, No. 1 +Title-Dates: 1884 + + + +Title-Name: The Wild Duck +Title-RAW: , folk song +Title-Opus: B. 140 +Title-Dates: 1884 + + + +Title-Type: Symphony +Title-No: 7 +Title-Key: D minor +X-Title-Opus-Alt: B. 141 +Title-Opus: Op. 70 +Title-Dates: 1885 + + + +Title-RAW: Two Czech Folk Poems +Title-Opus: B. 142 +Title-Dates: 1885 + + + +Title-Name: Hymn of the Czech Peasants +Title-Opus: B. 143 +Title-Dates: 1885 + + + +Title-Name: Saint Ludmila +Title-Type-After-Name: oratorio +X-Title-Opus-Alt: B. 144 +Title-Opus: Op. 71 +Title-Dates: 1886 + + + +Title-RAW: Slavonic Dances for piano four hands, 2nd series +X-Title-Opus-Alt: B. 145 +Title-Opus: Op. 72 +Title-Dates: 1886 + + + +Title-Name: In Folk Tone +Title-Comment: [collection of Slavonic and Czech folksongs] +X-Title-Opus-Alt: B. 146 +Title-Opus: Op. 73 +Title-Dates: 1886 + + + +Title-RAW: Slavonic Dances +Title-For: orchestra +Title-Comment: [orchestration of B. 145] +X-Title-Opus-Alt: B. 147 +Title-Opus: Op. 72 +Title-Dates: 1887 + + + +Title-RAW: Terzetto +Title-Key: C major +Title-For: two violins and 1 viola +X-Title-Opus-Alt: B. 148 +Title-Opus: Op. 74 +Title-Dates: 1887 + + + +Title-RAW: Miniatures +Title-For: two violins and violas +X-Title-Opus-Alt: B. 149 +Title-Opus: Op. 75a +Title-Dates: 1887 + + + +Title-RAW: Romantic Pieces +Title-For: violin and piano +X-Title-Opus-Alt: B. 150 +Title-Opus: Op. 75 +Title-Dates: 1887 + + + +Title-Name: The King and the Charcoalburner +Title-Comment: [3rd version of B. 21] +X-Title-Opus-Alt: B. 151 +Title-Opus: Op. 14 +Title-Dates: 1887 + + + +Title-Name: Cypresses +Title-Comment: [arrangements of a dozen pieces from B. 11 (Nos. 2-4, 6-9, 12, 14, 16-18)] +Title-Opus: B. 152 +Title-Dates: 1887 + + + +Title-Type: Mass +Title-Key: D major +Title-Comment: [1st version] +X-Title-Opus-Alt: B. 153 +Title-Opus: Op. 86 +Title-Dates: 1887 + + + +Title-RAW: Psalm 149 +Title-For: choir and orchestra +Title-Comment: [2nd version of B. 91] +X-Title-Opus-Alt: B. 154 +Title-Opus: Op. 79 +Title-Dates: 1887 + + + +Title-Type: Piano Quintet +Title-No: 2 +Title-Key: A major +X-Title-Opus-Alt: B. 155 +Title-Opus: Op. 81 +Title-Dates: 1887 + + + +Title-Name: Two Little Pearls +Title-For: piano +Title-Opus: B. 156 +Title-Dates: 1887 + + + +Title-RAW: Four Songs on Poems by O. Malybrok-Stieler +X-Title-Opus-Alt: B. 157 +Title-Opus: Op. 82 +Title-Dates: 1888 + + + +Title-RAW: Album Leaf +Title-For: piano +Title-Opus: B. 158 +Title-Dates: 1888 + + + +Title-Name: The Jacobin +Title-Type-After-Name: opera +X-Title-Opus-Alt: B. 159 +Title-Opus: Op. 84 +Title-Dates: 1888 + + + +Title-Name: Love Songs +Title-Comment: [revision of Cypresses, B. 11] +X-Title-Opus-Alt: B. 160 +Title-Opus: Op. 83 +Title-Dates: 1888 + + + +Title-Name: Poetic Tone Poems +Title-For: piano +X-Title-Opus-Alt: B. 161 +Title-Opus: Op. 85 +Title-Dates: 1889 + + + +Title-Type: Piano Quartet +Title-No: 2 +Title-Key: E flat major +X-Title-Opus-Alt: B. 162 +Title-Opus: Op. 87 +Title-Dates: 1889 + + + +Title-Type: Symphony +Title-No: 8 +Title-Key: G major +X-Title-Opus-Alt: B. 163 +Title-Opus: Op. 88 +Title-Dates: 1889 + + + +Title-Type: Gavotte +Title-For: three violins +Title-Opus: B. 164 +Title-Dates: 1890 + + + +Title-Type: Requiem +X-Title-Opus-Alt: B. 165 +Title-Opus: Op. 89 +Title-Dates: 1890 + + + +Title-Type: Piano Trio +Title-No: 4 +Title-Key: E minor +Title-Name: Dumky +X-Title-Opus-Alt: B. 166 +Title-Opus: Op. 90 +Title-Dates: 1891 + + + +Title-Type: Fanfares +Title-For: four trumpets and timpani +Title-Opus: B. 167 +Title-Dates: 1891 + + + +Title-Name: In Nature's Realm +Title-RAW: , concert overture +X-Title-Opus-Alt: B. 168 +Title-Opus: Op. 91 +Title-Dates: 1891 + + + +Title-Name: Carnival Overture +X-Title-Opus-Alt: B. 169 +Title-Opus: Op. 92 +Title-Dates: 1891 + + + +Title-RAW: Slavonic Dance +Title-Key: E minor +Title-For: violin and piano +Title-Comment: [arrangement of B. 78 No. 2] +X-Title-Opus-Alt: B. 170 +Title-Opus: Op. 46, No. 2 +Title-Dates: 1891 + + + +Title-Type: Rondo +Title-Key: G minor +Title-For: cello and piano +X-Title-Opus-Alt: B. 171 +Title-Opus: Op. 94 +Title-Dates: 1891 + + + +Title-RAW: Slavonic Dances in A and G minor +Title-For: cello and piano +Title-Comment: [arrangement of B. 78 Nos. 3 and 8] +X-Title-Opus-Alt: B. 172 +Title-Opus: Op. 46, No. 3,8 +Title-Dates: 1891 + + + +Title-Name: Silent Woods +Title-RAW: for cello and piano (Published as +Title-Name: Waldesruhe +Title-Punct: ) +Title-Comment: [arrangement of Ze Sumavy (From the Bohemian Forest) Op.68 / B.133 No.5] +X-Title-Opus-Alt: B. 173 +Title-Opus: Op. 68, No. 5 +Title-Dates: 1891 + + + +Title-Name: Othello +Title-RAW: Overture +X-Title-Opus-Alt: B. 174 +Title-Opus: Op. 93 +Title-Dates: 1892 + + + +Title-Type: Mass +Title-Key: D major +Title-Comment: [2nd version of B. 153] +X-Title-Opus-Alt: B. 175 +Title-Opus: Op. 86 +Title-Dates: 1892 + + + +Title-RAW: Te Deum +X-Title-Opus-Alt: B. 176 +Title-Opus: Op. 103 +Title-Dates: 1892 + + + +Title-Name: The American Flag +Title-Type-After-Name: cantata +Title-Comment: [after a poem by J. R. Drake] +X-Title-Opus-Alt: B. 177 +Title-Opus: Op. 102 +Title-Dates: 1893 + + + +Title-Type: Symphony +Title-No: 9 +Title-Key: E minor +Title-Name: From the New World +X-Title-Opus-Alt: B. 178 +Title-Opus: Op. 95 +Title-Dates: 1893 + + + +Title-Type: String Quartet +Title-No: 12 +Title-Key: F major +Title-Name: American +X-Title-Opus-Alt: B. 179 +Title-Opus: Op. 96 +Title-Dates: 1893 + + + +Title-Type: String Quintet +Title-No: 3 +Title-Key: E flat major +Title-Name: American +X-Title-Opus-Alt: B. 180 +Title-Opus: Op. 97 +Title-Dates: 1893 + + + +Title-Type: Rondo +Title-Key: G minor +Title-For: cello and orchestra +Title-Comment: [arrangement of B. 171] +X-Title-Opus-Alt: B. 181 +Title-Opus: Op. 94 +Title-Dates: 1893 + + + +Title-Name: Silent Woods +Title-For: cello and orchestra +Title-Comment: [arrangement of Ze Sumavy (From the Bohemian Forest) Op.68/B.173 No.5] +X-Title-Opus-Alt: B. 182 +Title-Opus: Op. 68, No. 5 +Title-Dates: 1893 + + + +Title-Type: Violin Sonatina +Title-Key: G major +X-Title-Opus-Alt: B. 183 +Title-Opus: Op. 100 +Title-Dates: 1893 + + + +Title-Type: Suite +Title-Key: A major +Title-Name: American +Title-For: piano +X-Title-Opus-Alt: B. 184 +Title-Opus: Op. 98 +Title-Dates: 1894 + + + +Title-RAW: Biblical Songs +X-Title-Opus-Alt: B. 185 +Title-Opus: Op. 99 +Title-Dates: 1894 + + + +Title-Name: Dmitrij +Title-Comment: [2nd version of B. 127] +X-Title-Opus-Alt: B. 186 +Title-Opus: Op. 64 +Title-Dates: 1894 + + + +Title-Type: Humoresques +Title-For: piano +X-Title-Opus-Alt: B. 187 +Title-Opus: Op. 101 +Title-Dates: 1894 + + + +Title-Count: Two +Title-Type: Piano Pieces +Title-Opus: B. 188 +Title-Dates: 1894 + + + +Title-RAW: Biblical Songs +Title-Comment: [orchestration of B. 185] +X-Title-Opus-Alt: B. 189 +Title-Opus: Op. 99 +Title-Dates: 1895 + + + +Title-Type: Suite +Title-Key: A major +Title-Name: American +Title-Comment: [orchestration of B. 184] +X-Title-Opus-Alt: B. 190 +Title-Opus: Op. 98b +Title-Dates: 1895 + + + +Title-Type: Cello Concerto +Title-Key: B minor +X-Title-Opus-Alt: B. 191 +Title-Opus: Op. 104 +Title-Dates: 1895 + + + +Title-Type: String Quartet +Title-No: 13 +Title-Key: G major +X-Title-Opus-Alt: B. 192 +Title-Opus: Op. 106 +Title-Dates: 1895 + + + +Title-Type: String Quartet +Title-No: 14 +Title-Key: A flat major +X-Title-Opus-Alt: B. 193 +Title-Opus: Op. 105 +Title-Dates: 1895 + + + +Title-Name: Lullaby +Title-Comment: [after a poem by F. L. Jelinek] +Title-Opus: B. 194 +Title-Dates: 1895 + + + +Title-Name: The Water Goblin +Title-Type-After-Name: symphonic poem +X-Title-Opus-Alt: B. 195 +Title-Opus: Op. 107 +Title-Dates: 1896 + + + +Title-Name: The Noon Witch +Title-Type-After-Name: symphonic poem +X-Title-Opus-Alt: B. 196 +Title-Opus: Op. 108 +Title-Dates: 1896 + + + +Title-Name: The Golden Spinning Wheel +Title-Type-After-Name: symphonic poem +X-Title-Opus-Alt: B. 197 +Title-Opus: Op. 109 +Title-Dates: 1896 + + + +Title-Name: The Wild Dove +Title-Type-After-Name: symphonic poem +X-Title-Opus-Alt: B. 198 +Title-Opus: Op. 110 +Title-Dates: 1896 + + + +Title-Name: A Hero's Song +Title-Type-After-Name: symphonic poem +X-Title-Opus-Alt: B. 199 +Title-Opus: Op. 111 +Title-Dates: 1897 + + + +Title-Name: Jacobin +Title-Comment: [revision of B. 159] +X-Title-Opus-Alt: B. 200 +Title-Opus: Op. 84 +Title-Dates: 1897 + + + +Title-Name: The Devil and Kate +Title-Type-After-Name: opera +X-Title-Opus-Alt: B. 201 +Title-Opus: Op. 112 +Title-Dates: 1899 + + + +Title-Type: Overture +Title-Related-How: to +Title-Related-Name: Kate and the Devil +X-Title-Opus-Alt: B. 201a +Title-Opus: Op. 112 +Title-Dates: 1899 + + + +Title-Name: Festival Song +Title-Comment: [after a poem by J. Vrchlicky] +X-Title-Opus-Alt: B. 202 +Title-Opus: Op. 113 +Title-Dates: 1900 + + + +Title-Name: Rusalka +Title-Type-After-Name: opera +X-Title-Opus-Alt: B. 203 +Title-Opus: Op. 114 +Title-Dates: 1900 + + + +Title-Type: Overture +Title-Related-How: to +Title-Related-Name: Rusalka +X-Title-Opus-Alt: B. 203a +Title-Opus: Op. 114 +Title-Dates: 1900 + + + +Title-Name: Song of the Smith of Lesetin +Title-Opus: B. 204 +Title-Dates: 1901 + + + +Title-Name: Saint Ludmila +Title-Comment: [opera adapted from cantata B. 144] +X-Title-Opus-Alt: B. 205 +Title-Opus: Op. 71 +Title-Dates: 1901 + + + +Title-Name: Armida +Title-Type-After-Name: opera +X-Title-Opus-Alt: B. 206 +Title-Opus: Op. 115 +Title-Dates: 1903 + + + +Title-Type: Overture +Title-Related-How: to +Title-Related-Name: Armida +X-Title-Opus-Alt: B. 206a +Title-Opus: Op. 115 +Title-Dates: 1903 + + + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Schnittke.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Schnittke.comp new file mode 100644 index 000000000..83e6fad6d --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/A_Schnittke.comp @@ -0,0 +1,2496 @@ +# format = mail-header + +## Raw data (and opus numbers) from http://home.wanadoo.nl/ovar/schnopus.htm + +## <H3>Works</H3> +## In fact Alfred Schnittke didn't use opus numbers. +## Here opus numbers are used to facilitate indexing.<P> + +Title-Type: Concerto +Title-For: accordion and orchestra +Title-Opus: 1 +Title-Dates: 1949 + +## <DL><DD>Lost</DL> + +Title-Type: Incidental music +Title-Related-How: to +Title-Related-Name: Mayakovsky's Debut +Title-Opus: 2 +Title-Dates: 1950-1952 + +Title-RAW: Poème +Title-For: piano and orchestra +Title-Opus: 3 +Title-Dates: 1953 + +Title-Name: The Passing Line of Clouds Grows Thinner +Title-For: voice and piano +Title-Opus: 4 +Title-Dates: 1953 + +## <DL><DD>To a poem by Alexander Pushkin.</DL> + +Title-Type: Fugue +Title-For: violin solo +Title-Opus: 5 +Title-Dates: 1953 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/kancheli/bis1392.htm">BIS CD 1392</A>: Vadim Gluzman (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554728.htm">Naxos 8.554728</A>: Mark Lubotsky (violin)</I></DL> + +Title-Count: Six +Title-Type: Preludes +Title-For: piano +Title-Opus: 6 +Title-Dates: 1953-1954 + +Title-Type: Variations +Title-For: piano +Title-Opus: 7 +Title-Dates: 1954-1955 + +Title-Type: Sonata +Title-For: violin and piano +Title-Opus: 8 +Title-Dates: 1954-1955 + +## <DL><DD><I>CD Warner Classics 2564 61329-2: Daniel Hope (violin), Sebastian Knauer (piano)</I></DL> + +Title-Name: Dusk (Sumrak) +Title-For: voice and piano +Title-Opus: 9 +Title-Dates: 1954-1955 + +## <DL><DD>To a poem by Fyodor Tyutchev.</DL> + +Title-Name: Beggar (Nishchy) +Title-For: voice and piano +Title-Opus: 10 +Title-Dates: 1954-1955 + +## <DL><DD>To a poem by Mikhail Lermontov.</DL> + +Title-Name: Birch Tree (Beryozka) +Title-For: voice and piano +Title-Opus: 11 +Title-Dates: 1954-1955 + +## <DL><DD>To a poem by Stepan Shchiparev.</DL> + +Title-Count: Three +Title-Type: Choruses +Title-For: mixed chorus +Title-Opus: 12 +Title-Dates: 1954/1955 + +## <DL><DD>To a poems by Alexander Prokofiev, Mikhail Isakovsky and Alexander Mashistov.<P> +## <I>CD Delos DE 3264: The Spiritual Revival Choir of Moscow's Schnittke Institute of Music, Lev Kontorovich (cond)</I></DL> + +Title-Type: Scherzo +Title-For: piano quintet +Title-Opus: 13 +Title-Dates: 1954-1955 + +Title-Type: Intermezzo +Title-For: piano quintet +Title-Opus: 14 +Title-Dates: 1954-1955 + +Title-Type: Suite +Title-For: strings +Title-Opus: 15 +Title-Dates: 1954-1955 + +Title-Type: Overture +Title-For: orchestra +Title-Opus: 16 +Title-Dates: 1954-1955 + +Title-Type: Symphony +Title-No: 0 +Title-Opus: 17 +Title-Dates: 1956-1957 + +Title-Type: Concerto +Title-No: 1 +Title-For: violin and orchestra +Title-Opus: 18 +Title-Dates: 1957 + +## <DL><DD>Revised in 1962.<BR> +## In four movements:<BR> +## 1. Allegro ma non troppo. Tempo iniziale - 13 min.<BR> +## 2. Presto (This movement may be omitted) - 5 min.<BR> +## 3. Andante - 10 min.<BR> +## 4. Allegro scherzando - 9 min.<P> +## <I>CD BIS CD 487: Malmö SO, Eri Klas (cond), Mark Lubotsky (violin)</I></DL> + +Title-Name: Nagasaki +Title-Type-After-Name: oratorio +Title-For: mezzo-soprano, mixed chorus and symphony orchestra +Title-Opus: 19 +Title-Dates: 1958 + +## <DL><DD>On a text by Anatoly Sofronov, Georgi Fere, Eneda Eisaku and Simedziku Toson.<BR> +## In five movements:<BR> +## 1. Nagasaki: city of grief<BR> +## 2. Morning (attacca)<BR> +## 3. That fateful day ...<BR> +## 4. After the holocaust<BR> +## 5. The sun of peace</DL> + +Title-Name: Vocalise +Title-For: mixed chorus a cappella +Title-Opus: 20 +Title-Dates: 1958 + +Title-RAW: Songs of War and Peace, cantata +Title-For: soprano, mixed chorus and symphony orchestra +Title-Opus: 21 +Title-Dates: 1959 + +## <DL><DD>On texts by Anatoly Leontyev and Andrei Pokrovsky, based on modern Russian folk songs.<BR> +## In four movements:<BR> +## 1. Golden grass on ancient burial mounds<BR> +## 2. War is rumbling in the fields<BR> +## 3. My heart moans<BR> +## 4. The storm has passed. The sky is clear</DL> + +Title-Type: String Quartet +Title-Opus: 22 +Title-Dates: 1959 + +## <DL><DD>Unfinished.</DL> + +Title-Type: Concerto +Title-For: piano and orchestra +Title-Opus: 23 +Title-Dates: 1960 + +## <DL><DD>1. Allegro<BR> +## 2. Andante (attacca)<BR> +## 3. Allegro</DL> + +Title-Type: Concerto +Title-For: electric instruments +Title-Opus: 24 +Title-Dates: 1960 + +## <DL><DD>Unfinished.</DL> + +Title-RAW: Poem about Cosmos +Title-For: orchestra +Title-Opus: 25 +Title-Dates: 1961 + +Title-Name: The Eleventh Commandment +Title-Type-After-Name: opera in two acts +Title-Opus: 26 +Title-Dates: 1962 + +## <DL><DD>Libretto by Marina Churova, Georgy Ansimov and Alfred Schnittke.</DL> + +Title-RAW: Children's Suite +Title-For: small orchestra +Title-Opus: 27 +Title-Dates: 1962 + +## <DL><DD>In six movements:<BR> +## 1. Moderato<BR> +## 2. Vivo<BR> +## 3. Moderato<BR> +## 4. Andantino<BR> +## 5. Allegro<BR> +## 6. Andantino</DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Introduction +Title-Opus: 28 +Title-Dates: 1962 + +Title-Type: Music +Title-Related-How: to the TV-production +Title-Related-Name: The Rose and the Cross +Title-Opus: 29 +Title-Dates: 1962 + +## <DL><DD>After Alexander Blok</DL> + +Title-Type: Sonata +Title-No: 1 +Title-For: violin and piano +Title-Opus: 30 +Title-Dates: 1963 + +## <DL><DD>In three movements:<BR> +## 1. Andante - 3 min.<BR> +## 2. Allegretto - 6 min.<BR> +## 3. Largo - 5 min.<BR> +## 4. Allegretto scherzando. Largo - 6 min.<P> +## <I>LP Chandos ABRD 1089: Rostislav Dubinsky (violin), Ljuba Edlina (piano)<BR> +## CD ASV CDDCA 868: Mateja Marinkovic (violin), Linn Hendry (piano)<BR> +## CD BIS CD 364: Christian Bergqvist (violin), Roland Pontinen (piano)<BR> +## CD BIS CD 527: U. Wallin (violin), Roland Pöntinen (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bridge9104.htm">Bridge 9104</A>: Joanna Kurkowicz (violin), Sergey Schepkin (piano)<BR> +## CD Catalyst 09026-62668-2: Maria Bachmann (violin), Jon Klibonoff (piano)<BR> +## CD Chandos CHAN 8343: Rostislav Dubinsky (violin), Ljuba Edlina (piano)<BR> +## CD Duchesne CD 71 532: I. Tseitlin (violin), P. Dheur (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/hmn911701.htm">Harmonia Mundi HMN 911701</A>: Graf Mourja (violin), Elena Rozanova(piano)<BR> +## CD Live Classics LCL 191: Oleg Kagan (violin), Vassily Lobanov (piano)<BR> +## CD Ondine ODE 800-2: Mark Lubotsky (violin), Ralf Gothoni (piano)<BR> +## CD Ondine ODE 901-2: Pekka Kuusisto (violin), Raija Kerppo (piano)<BR> +## CD Proud Sound PROU CD 139: Rusne Mataityte (violin), Margrit-Julia Zimmermann (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/sp22579.htm">Sonora Products SO 22579 CD</A>: Valery Gradow (violin), Inna Heifitz (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)<BR> +## CD Vienna Modern Masters VMM 2025: Vasilij Meljnikov (violin), Aljoscha Starc (piano)</I></DL> + +Title-RAW: Prelude and Fugue +Title-For: piano +Title-Opus: 31 +Title-Dates: 1963 + +## <DL><DD>1. Andante<BR> +## 2. Allegretto<P> +## <I>CD Chandos CHAN 9704: Boris Berman (piano)</I></DL> + +Title-Type: Incidental music +Title-Related-How: to +Title-Related-Name: Caesar and Cleopatra +Title-Opus: 32 +Title-Dates: 1963 + +## <DL><DD>Play in five acts by George Bernard Shaw.</DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Aim the Barrage at Us +Title-Opus: 33 +Title-Dates: 1963-1964 + +Title-Type: Music +Title-For: piano and chamberorchestra +Title-Opus: 34 +Title-Dates: 1964 + +## <DL><DD>In four movements:<BR> +## 1. Variazioni<BR> +## 2. Cantus firmus<BR> +## 3. Cadenza (attacca)<BR> +## 4. Basso ostinato<P> +## <I>CD Chandos CHAN 9466: Russian State SO, Gennadi Rozhdestvensky (cond), Vassily Lobanov, Vassily (piano)</I></DL> + +Title-Type: Music +Title-For: chamberorchestra +Title-Opus: 35 +Title-Dates: 1964 + +Title-RAW: Three Verses of Marina Tsvetayeva +Title-For: mezzo-soprano (or soprano) and piano +Title-Opus: 36 +Title-Dates: 1965 + +Title-Type: Dialogue +Title-For: cello and seven instrumentalists +Title-Opus: 37 +Title-Dates: 1965 + +## <DL><DD>Duration: 12 minutes.<P> +## <I>CD BIS CD 568: (Arrangement for trombone by Ch. Lindberg and A. Schnittke) Tapiola Sinfonietta, Osmo Vänskä (cond), Christian Lindberg (trombone)<BR> +## CD BIS CD 568: (Arrangement for trombone by Ch. Lindberg and A. Schnittke) Tapiola Sinfonietta, Osmo Vänskä (cond), Christian Lindberg (trombone)<BR> +## CD Live Classics LCL 202: Gnessin Chamber Orchestra, Yuri Nikolaevsky (cond), Natalia Gutman (cello)</I></DL> + +Title-RAW: Improvisation and Fugue +Title-For: piano +Title-Opus: 38 +Title-Dates: 1965 + +## <DL><DD>1. Lento<BR> +## 2. Vivo<P> +## <I>CD Chandos CHAN 9704: Boris Berman (piano)</I></DL> + +Title-RAW: Variations on One Chord +Title-For: piano +Title-Opus: 39 +Title-Dates: 1965 + +## <DL><DD>Grave / Lento / Allegretto / Andante / Agitato / Lento / Maestoso / Andante<P> +## <I>CD Chandos CHAN 9704: Boris Berman (piano)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Adventures of a Dentist +Title-Opus: 40 +Title-Dates: 1965 + +Title-RAW: Charleston +Title-For: stage orchestra/jazz ensemble +Title-Related-How: from the Film +Title-Related-Name: The Adventures of a Dentist +Title-Opus: 40a +Title-Dates: 1965 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Concealed Caballero +Title-Opus: 41 +Title-Dates: 1965 + +Title-Type: Concerto +Title-No: 2 +Title-For: violin and chamber orchestra +Title-In-Movements: in a single movement +Title-Opus: 42 +Title-Dates: 1966 + +## <DL><DD>Dedicated to Mark Lubotsky. +## Duration: 23 minutes.<P> +## <I>LP Philips 411 107-1: Basle Symphony Orchestra, Heinz Holliger (cond), Gidon Kremer (violin)<BR> +## CD BIS CD 487: Malmö SO, Eri Klas (cond), Mark Lubotsky (violin)<BR> +## CD Teldec 4509-94540-2: Chamber Orchestra of Europe, Gidon Kremer (violin)</I></DL> + +Title-Type: String Quartet +Title-No: 1 +Title-Opus: 43 +Title-Dates: 1966 + +## <DL><DD>Commissioned by Rostislav Dubinsky, Primarius of the Borodin Quartet.<BR> +## In three movements:<BR> +## 1. Sonata - 8 min.<BR> +## 2. Canon - 4 min.<BR> +## 3. Cadenza - 7 min.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/arco54.htm">Arco Diva UP 0054-2 131</A>: Kapralova Quartet<BR> +## CD BIS CD 467: Tale Quartet<BR> +## CD Nonesuch 79500-2: Kronos Quartet</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Just a Little Joke +Title-Opus: 44 +Title-Dates: 1966 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Day Stars +Title-Opus: 45 +Title-Dates: 1966 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Commissar +Title-Opus: 46 +Title-Dates: 1967 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cap71041.htm">Capriccio hybrid CD/SACD CC 71041</A>: Berlin Radio Symphony Orchestra, Frank Strobel (cond)</I></DL> + +Title-Name: Pianissimo +Title-For: large symphony orchestra +Title-Opus: 47 +Title-Dates: 1968 + +## <DL><DD>Duration: 9 minutes.<P> +## <I>CD BIS CD 427: Gothenburg SO, Neeme Jarvi (cond)</I></DL> + +Title-Type: Serenada +Title-For: violin, clarinet, double-bass, piano and percussion +Title-Opus: 48 +Title-Dates: 1968 + +## <DL><DD>1. -<BR> +## 2. Lento<BR> +## 3. Allegretto<P> +## <I>CD Hyperion CDA 66885: Capricorn, Timothy Mason (cond)</I></DL> + +Title-Type: Sonata +Title-No: 2 +Title-For: violin and piano +Title-Name: Quasi una sonata +Title-In-Movements: in a single movement +Title-Opus: 49 +Title-Dates: 1968 + +## <DL><DD>Dedicated to Mark Lubotsky and Lyubov Yedlina.<P> +## <I>CD ASV CDDCA 868: Mateja Marinkovic (violin), Linn Hendry (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bridge9104.htm">Bridge 9104</A>: Joanna Kurkowicz (violin), Sergey Schepkin (piano)<BR> +## CD Duchesne CD 71 532: I. Tseitlin (violin), P. Dheur (piano)<BR> +## CD Northern Flowers NF 9908: Lidia Kovalenko (violin), Yuri Serov (piano)<BR> +## CD Ondine ODE 800-2: Mark Lubotsky (violin), Ralf Gothoni (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/phoenix150.htm">Phoenix PHCD 150</A>: Levon Ambartsumian (violin), Anatoly Sheludyakov (piano)<BR> +## CD Russian Disc RDCD 10001: Levon Ambartsumian (violin), Anatoly Sheludyakov (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/sp22579.htm">Sonora Products SO 22579 CD</A>: Valery Gradow (violin), Inna Heifitz (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)</I></DL> + +Title-Type: Sonata +Title-For: violin and chamber orchestra +Title-Opus: 50 +Title-Dates: 1968 + +## <DL><DD>Arrangement of Violin Sonata no. 1 (1963).<BR> +## 1. Andante - 3 min.<BR> +## 2. Allegretto - 5 min. 30 sec.<BR> +## 3. Largo - 5 min. 30 sec.<BR> +## 4. Allegretto scherzando. Allegro - 5 min. 30 sec.<P> +## <I>CD BIS CD 537: Stockholm Chamber Orchestra, Lev Markiz (cond), Christian Bergqvist (violin)<BR> +## CD Capriccio 67 016: Moscow Virtuosi, V. Spivakov (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan9891.htm">Chandos CHAN 9891</A>: Musici de Montreal, Yuli Turovsky (conductor), Stepan Arman (violin)<BR> +## CD Nimbus NI 5582: English SO, William Boughton (cond), Daniel Hope (violin)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Angel +Title-Opus: 51 +Title-Dates: 1968 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: July the Sixth +Title-Opus: 52 +Title-Dates: 1968 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Day Stars +Title-Opus: 53 +Title-Dates: 1968 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Ownerlewss House +Title-Opus: 54 +Title-Dates: 1968 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Glass Harmonica +Title-Opus: 55 +Title-Dates: 1968 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cap71061.htm">Capriccio 71061</A> (SACD): Rundfunk-Sinfonieorchester Berlin, Frank Strobel (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Used Cartridge Cases +Title-Opus: 56 +Title-Dates: 1968 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Night Call +Title-Opus: 57 +Title-Dates: 1968 + +Title-RAW: Magdalena's Song for voice and piano on words by Boris Pasternak +Title-Opus: 58 +Title-Dates: 1968 + +Title-RAW: "Potok" ("Stream"), electronic composition +Title-Opus: 59 +Title-Dates: 1969 + +## <DL><DD><I>Melodiya C 30721</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Waltz +Title-Opus: 60 +Title-Dates: 1969 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cap71061.htm">Capriccio 71061</A> (SACD): Rundfunk-Sinfonieorchester Berlin, Frank Strobel (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Sick at Heart +Title-Opus: 61 +Title-Dates: 1969 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: A Ballerina Aboard +Title-Opus: 62 +Title-Dates: 1969 + +Title-RAW: Incidental music to Alexander Pushkin's poem +Title-Name: Boris Godunov +Title-Opus: 63 +Title-Dates: 1969 + +Title-Type: Concerto +Title-For: oboe, harp and string orchestra +Title-In-Movements: in a single movement +Title-Opus: 64 +Title-Dates: 1971 + +## <DL><DD>Dedicated to Heinz Holliger, Ursula Holliger and the Zagreb Soloists Chamber Orchestra.<BR> +## Lento (one movement)<BR> +## Duration: 17 minutes.<P> +## <I>CD BIS CD 377: New Stockholm Chamber Orchestra, Lev Markiz (cond), Helen Jahren (oboe), Kjell Axel Lier (harp)</I></DL> + +Title-RAW: Canon in Memoriam Igor Strawinsky +Title-For: string quartet +Title-Opus: 65 +Title-Dates: 1971 + +## <DL><DD>Commissioned by the music magazine 'Tempo', London.<BR> +## Lento (one movement).<BR> +## Duration: 4 minutes 30 seconds.<P> +## <I>CD BIS CD 547: Tale Quartet<BR> +## CD DG 431 686-2: Hagen Quartet<BR> +## CD Etcetera KTC 1124: Mondriaan Quartet<BR> +## CD Nonesuch 79500-2: Kronos Quartet<BR> +## CD RCA-BMG Catalyst 82876 64283-2: Chilingirian Quartet: Levon Chilingirian (violin), Charles Sewart (violin), Simon Rowland-Jones (viola), Philip De Groote (cello)</I></DL> + +Title-Count: Eight +Title-Type: Pieces +Title-For: piano +Title-Opus: 66 +Title-Dates: 1971 + +## <DL><DD>1. Folk Song Andantino<BR> +## 2. In the Mountains Moderato<BR> +## 3. Cuckoo and Woodpecker Vivo<BR> +## 4. Melody Andante<BR> +## 5. Tale Lento<BR> +## 6. Play Allegro<BR> +## 7. Children's Piece Andantino<BR> +## 8. March Allegretto<P> +## <I>CD Chandos CHAN 9704: (Four Pieces) Boris Berman (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/mv06.htm">MV Productions SPPS CD06</A>: Svetlana Ponomarëva (piano)</I></DL> + +Title-Name: Labyrinths +Title-Type-After-Name: ballet +Title-In-Movements: in five episodes +Title-Opus: 67 +Title-Dates: 1971 + +## <DL><DD>1. Moderato. Allegretto. Meno mosso. Adagio - 12 min. 30 sec.<BR> +## 2. Moderato - 3 min. 30 sec.<BR> +## 3. Allegretto - 2 min.<BR> +## 4. Agitato - 4 min.<BR> +## 5. Cadenza. Andante. Maestoso - 16 min.<P> +## <I>CD BIS CD 557: Malmö SO Chamber Ensemble, Lev Markiz (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Belorussian Station +Title-Opus: 68 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Uncle Vanya +Title-Opus: 69 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Sport, Sport, Sport +Title-Opus: 70 +Title-Dates: 1971 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/ocd606.htm">Olympia OCD 606</A>: USSR Cinematography SO, Emin Khachaturian (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Seagull +Title-Opus: 71 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Our Gagarin +Title-Opus: 72 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Last Flight of the Albatross +Title-Opus: 73 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: You and Me +Title-Opus: 74 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: A Cottage in Kolomna +Title-Opus: 75 +Title-Dates: 1971 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Wardrobe +Title-Opus: 76 +Title-Dates: 1971 + +Title-Name: Verses Written in the Sleeplessness of the Night +Title-For: voice and piano +Title-Opus: 77 +Title-Dates: 1971 + +## <DL><DD>On verses by Alexander Pushkin.<BR> +## Originally composed for a television production.</DL> + +Title-Type: Symphony +Title-No: 1 +Title-Opus: 78 +Title-Dates: 1969-1972 + +## <DL><DD>In four movements:<BR> +## 1. Senza tempo / Moderato / Allegro / Andante - 21 min.<BR> +## 2. Allegretto - 15 min.<BR> +## 3. Lento (attacca) - 9 min.<BR> +## 4. Lento - 27 min.<P> +## <I>CD BIS CD 577: Royal Stockholm PO, Leif Segerstam (cond), Ben Kallenberg (violin), Ake Lannerholm (trombone), Carl-Axel Dominique (piano)<BR> +## CD Chandos CHAN 9417: Russian State SO, Gennadi Rozhdestvensky (cond)<BR> +## CD Melodiya SUCD 10 00062: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)</I></DL> + +Title-RAW: "Voices of Nature" (without text) +Title-For: ten female voices and vibraphone +Title-Opus: 79 +Title-Dates: 1972 + +## <DL><DD>Lento (one movement).<BR> +## Duration: 5 minutes.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1157.htm">BIS 1157</A>: Swedish Radio Choir, Tonu Kaljuste (cond), Jonny Ronnlund (vibraphone)<BR> +## CD Chandos CHAN 9480: Danish National Radio Choir, Stefan Parkman (cond), Gert Sorensen (vibraphone)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cda67297.htm">Hyperion CDA 67297</A>: Holst Singers, Stephen Layton (cond)</I></DL> + +Title-RAW: Suite in Old Style +Title-For: violin and piano (or harpsichord) +Title-Opus: 80 +Title-Dates: 1972 + +## <DL><DD>In five movements:<BR> +## 1.Pastorale - 4 min.<BR> +## 2. Ballet - 2 min.<BR> +## 3. Minuet - 4 min.<BR> +## 4. Fugue - 2 min. 30 sec.<BR> +## 5. Pantomime - 3 min. 30 sec.<P> +## <I>LP Chandos ABRD 1089: Rostislav Dubinsky (violin), Ljuba Edlina (piano)<BR> +## LP Melodiya C10 20223 005: V. Kafelnikov (trumpet), L. Grabko (piano)<BR> +## LP Melodiya C10 25671 007: E. Grach (violin), A. Maloletkova (piano)<BR> +## CD Arco Diva UP 0077-2: Ivana Tomaskova (violin), Renata Ardasevova (piano)<BR> +## CD ASV CDDCA 877: Mateja Marinkovic (violin), Linn Hendry (piano)<BR> +## CD BIS CD 527: U. Wallin (violin), R. Pöntinen (piano)<BR> +## CD <A HREF="/ovar/sovrev/kancheli/bis1392.htm">BIS CD 1392</A>: Vadim Gluzman (violin), Angela Yoffe (piano)<BR> +## CD Chandos CHAN 8343: Rostislav Dubinsky (violin), Ljuba Edlina (piano)<BR> +## CD Duchesne CD 71 532: I. Tseitlin (violin), P. Dheur (piano)<BR> +## CD Ondine ODE 800-2: Mark Lubotsky (violin), Ralf Gothoni (piano)<BR> +## CD Philips 456-016-2: (Arrangement by Gidon Kremer) Gidon Kremer (violin), Naoko Yoshino (harpsichord)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/sp22579.htm">Sonora Products SO 22579 CD</A>: Valery Gradow (violin), Inna Heifitz (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)<BR> +## CD Talent DOM 2910 125: Daniel Rubenstein (violin), Muhiddin Dürrüoglu-Demiriz (piano)</I></DL> + +Title-RAW: Suite in Old Style +Title-For: cello and piano +Title-Opus: 80a + +## <DL><DD>Arranged by Daniel Shafran.<BR> +## In five movements:<BR> +## 1.Pastorale - 4 min.<BR> +## 2. Ballet - 2 min.<BR> +## 3. Minuet - 4 min.<BR> +## 4. Fugue - 2 min. 30 sec.<BR> +## 5. Pantomime - 3 min. 30 sec.<P> +## <I>CD <A HREF="/ovar/sovrev/kancheli/asv4006.htm">ASV Gold GLD 4006</A>: Nikolai Demidenko (cello), Leonid Gorokhov (piano)<BR> +## CD Brilliant Classics 93096 (7 CD-set): Daniel Shafran (cello), Anton Ginzburg (piano)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Chile in Struggle, Hope and Alarm +Title-Opus: 81 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Cheer Up, the Worst is Yet to Come +Title-Opus: 82 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Butterfly +Title-Opus: 83 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Strange Little Frog +Title-Opus: 84 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Where the Arbat crosses Bubulinas Street +Title-Opus: 85 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Hot Snow +Title-Opus: 86 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Right to Jump +Title-Opus: 87 +Title-Dates: 1972 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: In the World of the Fables +Title-Opus: 88 +Title-Dates: 1973 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Arduous Roads of Peace/The Balance of Terror +Title-Opus: 89 +Title-Dates: 1973 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: My Past and My Thoughts +Title-Opus: 90 +Title-Dates: 1973 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cpo999796.htm">CPO 999 796-2</A>: Radio SO Berlin, Frank Strobel (cond)</I></DL> + +Title-RAW: Gratulationsrondo +Title-Key: C major +Title-For: violin and piano +Title-Opus: 91 +Title-Dates: 1973 + +## <DL><DD>Dedicated to Rostislav Dubinsky on occasion of his 50th birthday.<BR> +## Allegro (one movement).<BR> +## Duration: 10 minutes.<P> +## <I>CD ASV CDDCA 877: Mateja Marinkovic (violin), Linn Hendry (piano)<BR> +## CD BIS CD 527: U. Wallin (violin), R. Pöntinen (piano)<BR> +## CD Northern Flowers NF 9908: Lidia Kovalenko (violin), Yuri Serov (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)<BR> +## CD Teldec 4509-94540-2: Gidon Kremer (violin), Christoph Eschenbach (piano)</I></DL> + +Title-Type: Incidental music +Title-Related-How: to Bertold Brecht's play +Title-Related-Name: Turandot +Title-Opus: 92 +Title-Dates: 1973 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The World Today/And Yet I Believe +Title-Opus: 93 +Title-Dates: 1972-1974 + +Title-RAW: "Der Gelbe Klang" ("Yellow Sound"), scenic composition +Title-For: pantomime, instrumental ensemble and tape (mixed chorus) +Title-Opus: 94 +Title-Dates: 1973-1974 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Agony of Death +Title-Opus: 95 +Title-Dates: 1973-1974 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cpo999796.htm">CPO 999 796-2</A>: Radio SO Berlin, Frank Strobel (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/ocd606.htm">Olympia OCD 606</A>: USSR Cinematography SO, Emin Khachaturian (cond)</I></DL> + +Title-RAW: Hymn I +Title-For: cello, harp and timpani +Title-Opus: 96 +Title-Dates: 1974 + +## <DL><DD>Duration: 11 minutes.<P> +## <I>LP Melodiya C10 28753 008: A. Ivashkin (cello), I. Pashinskaya (harp), V. Grishin (percussion)<BR> +## CD BIS CD 507: Torleif Thédéen (cello), Ingegerd Fredlund (harp), Anders Holdar (percussion)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Torleif Thédéen (cello), Ingegerd Fredlund (harp), Anders Holdar (percussion)<BR> +## CD Melodiya SUCD 10 00061: A. Ivashkin (cello), I. Pashinskaya (harp), V. Grishin (percussion)</I></DL> + +Title-RAW: Hymn II +Title-For: cello and double-bass +Title-Opus: 97 +Title-Dates: 1974 + +## <DL><DD>Duration: 6 minutes.<P> +## <I>LP Melodiya C10 28753 008: A. Ivashkin (cello), V. Bartsalkin (double-bass)<BR> +## CD BIS CD 507: Torleif Thédéen (cello), Entcho Radoukanov (double-bass)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Torleif Thédéen (cello), Entcho Radoukanov (double-bass)<BR> +## CD Melodiya SUCD 10 00061: A. Ivashkin (cello), V. Bartsalkin (double-bass)</I></DL> + +Title-RAW: Hymn III +Title-For: cello, bassoon, harpsichord and bells or timpani +Title-Opus: 98 +Title-Dates: 1974 + +## <DL><DD>Duration: 5 minutes.<P> +## <I>LP Melodiya C10 28753 008: A. Ivashkin (cello), Y. Rudometkin (bassoon), V. Chasovennaya (harpsichord), V. Grishin (bells)<BR> +## CD BIS CD 507: Torleif Thédéen (cello), Christian Davidsson (bassoon), Mayumi Kamata (harpsichord), Anders Loguin (percussion)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Torleif Thédéen (cello), Christian Davidsson (bassoon), Mayumi Kamata (harpsichord), Anders Loguin (percussion)<BR> +## CD Melodiya SUCD 10 00061: A. Ivashkin (cello), Y. Rudometkin (bassoon), V. Chasovennaya (harpsichord), V. Grishin (bells)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Cities and Years +Title-Opus: 99 +Title-Dates: 1974 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Captain's Daughter +Title-Opus: 100 +Title-Dates: 1974 + +Title-RAW: Requiem, music to Schiller's Drama +Title-Name: Don Carlos +Title-For: soloists, mixed chorus and instrumental ensemble +Title-Opus: 101 +Title-Dates: 1975 + +## <DL><DD>In fourteen movements<BR> +## 1. Requiem - 3 min. 30 sec.<BR> +## 2. Kyrie - 2 min.<BR> +## 3. Dies irae - 1 min.<BR> +## 4. Tuba mirum - 4 min.<BR> +## 5. Rex tremendae majestatis - 1 min.<BR> +## 6. Recordare - 2 min. 30 sec.<BR> +## 7. Lacrimosa - 2 min. 30 sec.<BR> +## 8. Domine Jesu - 1 min. 30 sec.<BR> +## 9. Hostias - 1 min.<BR> +## 10. Sanctus - 4 min.<BR> +## 11. Benedictus - 2 min.<BR> +## 12. Agnus Dei - 2 min.<BR> +## 13. Credo - 4 min.<BR> +## 14. Requiem - 3 min. 30 sec.<P> +## <I>CD BIS CD 497: Stockholm Sinfonietta, Uppsala Academic Chamber Choir, Stefan Parkman (cond), Kristina Hjartsjo Salomonsson, Ingela Hogeras Sjoberg & Lisbeth Lindholm (soprano), Annika Finnila Eker (contralto), Nils Hogman (tenor)<BR> +## CD CHAN 9564: Russian State Symphony Orchestra, Russian State Symphonic Capella, Valery Polyansky (cond), Olga Sizova (soprano), Anaida Agadzhanian (soprano), Olga Tal (soprano), Tatiana Sharova (soprano), Ludmilla Kuznetsova (mezzo-soprano), Vsevolod Grivnov (tenor)<BR> +## CD Panton 81 1374-221: Prague Symphony Orchestra, Kuhn Mixed Chorus, Jiri Belohlavek (cond), Zdena Kloubova (soprano)</I></DL> + +Title-Name: Pantomime +Title-Type-After-Name: suite +Title-For: chamber orchestra +Title-Opus: 102 +Title-Dates: 1975 + +## <DL><DD>After W.A. Mozart's Fragment KV 416d.<BR> +## 1. Pantalone and Colombine<BR> +## 2. The Dottore<BR> +## 3. Pierrot<BR> +## 4. The Turk<BR> +## 5. Pierrot chasing Harlekin<BR> +## 6. Harlekin's Death<BR> +## 7. Pierrot is terrified<BR> +## 8. Finale</DL> + +Title-Name: Cantus Perpetuus +Title-For: keyboard instrument and percussion +Title-Opus: 103 +Title-Dates: 1975 + +Title-RAW: Preludium in Memoriam Dmitri Shostakovich +Title-For: two violins (or solo violin and magnetic tape) +Title-Opus: 104 +Title-Dates: 1975 + +## <DL><DD>Andante (one movement).<BR> +## Duration: 5 minutes.<P> +## <I>LP HMV-Melodiya ASD 3547: Gidon Kremer (violin)<BR> +## CD ASV CDDCA 877: Mateja Marinkovic & Thomas Bowes (violin)<BR> +## CD Aurophon AU 31899<BR> +## CD BIS CD 697: Oleg Krysa & Alexander Fischer (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bridge9104.htm">Bridge 9104</A>: Joanna Kurkowicz (violin), Sergey Schepkin (piano)<BR> +## CD Capriccio 67 115: V. Spivakov (violin)<BR> +## CD Col legno 0647 287: V. Spivakov (violin)<BR> +## CD DG 449 966-2: Gidon Kremer (violin)<BR> +## CD Duchesne CD 71 532: I. Tseitlin & P. Dheur (violin)<BR> +## CD RCA Victor 74321-24894-2: Sasha Rozhdestvensky & Vladimir Spivakov (violin)</I></DL> + +Title-RAW: Cadenza to Mozarts Piano Concerto in C minor KV 491 (first movement) +Title-Opus: 105 +Title-Dates: 1975 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Autumn +Title-Opus: 106 +Title-Dates: 1975 + +Title-RAW: Eight Songs from the incidental music to Schiller's +Title-Name: Don Carlos +Title-For: voice and piano or guitar +Title-Opus: 107 +Title-Dates: 1975 + +## <DL><DD>1. Prelude<BR> +## 2. Hope<BR> +## 3. A Path in the Mountains<BR> +## 4. Evil Monks<BR> +## 5. Love Song<BR> +## 6. Abouth Theatre<BR> +## 7. To my Friends<BR> +## 8. Song of the Marauders</DL> + +Title-Type: Piano Quintet +Title-Opus: 108 +Title-Dates: 1972-1976 + +## <DL><DD>In five movements:<BR> +## 1. Moderato (attacca) - 6 min.<BR> +## 2. Tempo di Valse - 5 min.<BR> +## 3. Andante - 6 min.<BR> +## 4. Lento (attacca) - 5 min.<BR> +## 5. Moderato pastorale - 3 min. 30 sec.<P> +## <I>LP Philips 411 107-1: Gidon Kremer (violin), Kathrin Rabus (violin), Gerard Causse (viola), Ko Iwasaki (cello), Elena Bashkirova (piano)<BR> +## CD Arabesque Z 6707: Lark Quartet, Gary Graffman (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/asv6251.htm">ASV Quicksilva CD QS 6251</A>: Barbican Piano Trio, Jan Peter Schmolck (violin), James Boyd (viola)<BR> +## CD BIS CD 547: Tale Quartet, Roland Pöntinen (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bbm1093.htm">Black Box BBM 1093</A>: Barbican Piano Trio, Jan Peter Schmolck (violin), James Boyd (viola)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/ecm1755.htm">ECM New Series 1755 (461 815-2)</A>: Keller Quartet, Alexei Lubimov (piano)<BR> +## CD Etcetera KTC 1124: Mondriaan Quartet, Fred Oldenburg (piano)<BR> +## CD Hyperion CDA 66885: Capricorn, Timothy Mason (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554728.htm">Naxos 8.554728</A>: Mark Lubotsky & Dimity Hall (violin), Theodore Kuchar & Irini Morozova (viola), Alexander Ivashkin & Julian Smiles(cello), Irina Schnittke (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554830.htm">Naxos 8.554830</A>: Vermeer Quartet, Boris Berman (piano)<BR> +## CD Northern Flowers NF 9908: Lidia Kovalenko & Alexey Baev (violin), Alexey Popov (viola), Kirill Timofeev (cello), Yuri Serov (piano)<BR> +## CD Russian Disc RDCD 10 031: Moscow String Quartet, Constantine Orbelian (piano)<BR> +## CD Virgin Classics VC 7 91436-2: Borodin Quartet, Ludmilla Berlinsky (piano)<BR> +## CD <A HREF="/ovar/shosrev/naxos8554830.htm">Naxos 8.554830</A>: Vermeer Quartet, B. Berman (piano)</I></DL> + +Title-RAW: "Der Sonnengesang des Franz von Assisi" for two mixed choruses and six instrumentalists +Title-Opus: 109 +Title-Dates: 1976 + +## <DL><DD><I>LP Melodiya: USSR State Chamber Chorus, Ensemble of Bolshoi Theatre Soloists, Valery Polyansky (cond)</I></DL> + +Title-RAW: Transcription of Shostakovich's Preludes nos. 1 & 2 (from "Five preludes") +Title-For: small orchestra +Title-Opus: 110 +Title-Dates: 1976 + +Title-RAW: Moz-Art for two violins, after Mozart KV 416 +Title-Opus: 111 +Title-Dates: 1976 + +## <DL><DD>Allegretto / Allegro (one movement)><BR> +## Duration: 5 minutes 30 seconds.<P> +## <I>CD BIS CD 697: Oleg Krysa & Alexander Fischer (violin)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Selecting a Target +Title-Opus: 112 +Title-Dates: 1976 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Legend on How Tsar Peter Got the Negro Married +Title-Opus: 113 +Title-Dates: 1976 + +Title-Count: Two +Title-Type: Fragments +Title-For: small symphony orchestra +Title-Related-How: from the music to the film +Title-Related-Name: The Tale of Tsar Peter +Title-Opus: 113a +Title-Dates: 1976 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Lion-Trainers +Title-Opus: 114 +Title-Dates: 1976 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Clowns and Kids +Title-Opus: 115 +Title-Dates: 1976 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cap71061.htm">Capriccio 71061</A> (SACD): Rundfunk-Sinfonieorchester Berlin, Frank Strobel (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Ricky-Ticky-Tari +Title-Opus: 116 +Title-Dates: 1976 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The White Steamer +Title-Opus: 117 +Title-Dates: 1976 + +Title-RAW: Two Cadenzas to Beethoven's Violin Concerto in D major Op. 61 +Title-For: solo violin, 10 violins and timpani +Title-Opus: 118 +Title-Dates: 1975-1977 + +Title-Type: Concerto Grosso +Title-No: 1 +Title-For: two violins, prepared piano, harpsichord and 21 strings +Title-Opus: 119 +Title-Dates: 1977 + +## <DL><DD>In six movements:<BR> +## 1. Preludio (attacca) - 6 min.<BR> +## 2. Toccata (attacca) - 5 min.<BR> +## 3. Recitativo - 9 min.<BR> +## 4. Cadenza (attacca) - 2 min. 30 sec.<BR> +## 5. Rondò (attacca) - 7 min. 30 sec.<BR> +## 6. Postludio - 3 min.<P> +## <I>CD BIS CD 377: New Stockholm Chamber Orchestra, Lev Markiz (cond), Christian Bergqvist (violin), P. Swedrup (violin), R. Pontinen (piano)<BR> +## CD BIS CD 1507: New Stockholm Chamber Orchestra, Lev Markiz (cond), Christian Bergqvist (violin), P. Swedrup (violin), R. Pontinen (piano)<BR> +## CD Chandos CHAN 9590: I Musici de Montreal, Yuli Turovsky (cond), Natalya Turovsky, Catherine Perrin<BR> +## CD DG 429 413-2: Chamber Orchestra of Europe, Heinrich Schiff (cond), Gidon Kremer (violin), Tatiana Grindenko (violin), Yuri Smirnov (harpsichord and piano)<BR> +## CD DG 439 452-2: Chamber Orchestra of Europe, Heinrich Schiff (cond), Gidon Kremer (violin), Tatiana Grindenko (violin), Yuri Smirnov (harpsichord and piano)<BR> +## CD DG 445 520-2: Chamber Orchestra of Europe, Heinrich Schiff (cond), Gidon Kremer (violin), Tatiana Grindenko (violin), Yuri Smirnov (harpsichord and piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/dg471626.htm">DG 471 626-2</A>: Chamber Orchestra of Europe, Heinrich Schiff (cond), Gidon Kremer (violin), Tatiana Grindenko (violin), Yuri Smirnov (harpsichord and piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cdman175.htm">Manchester Classical Gallery CDMAN 175</A>: St. Petersburg Mozarteum Chamber Orchestra, Arcady Shteinlukht (cond), Victor Kuleshov (violin), Ilia Ioff (violin), Julia Lev (harpsichord and piano)<BR> +## CD RCA Victor Gold Seal GD 60957: London SO, Gennadi Rozhdestvensky (cond), Gidon Kremer & Tatiana Grindenko (violin)<BR> +## CD RCA Victor 74321-24894-2 (2 CD-set): London SO, Gennadi Rozhdestvensky (cond), Gidon Kremer & Tatiana Grindenko (violin)<BR> +## CD Sikorski SIK 7-003E: London SO, Gennadi Rozhdestvensky (cond), Gidon Kremer & Tatiana Grindenko (violin)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Human Requitale +Title-Opus: 120 +Title-Dates: 1977 + +Title-RAW: "Magdalina" for voice and piano to a poem by Boris Pasternak +Title-Opus: 121 +Title-Dates: 1977 + +## <DL><DD>Part of Hommage to Zhivago (1993)</DL> + +Title-RAW: Moz-Art a la Haydn +Title-For: two violins and eleven strings +Title-Opus: 122 +Title-Dates: 1977 + +## <DL><DD>For 6 violins, 2 violas, 2 cellos and 1 double-bass.<BR> +## Dedicated to Tatyana Grindenko and Gidon Kremer.<P> +## <I>CD ASV CDDCA 877: Mateja Marinkovic & Thomas Bowes (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis1437.htm">BIS CD 1437</A>: Tapiola Sinfonietta, Ralf Gothoni (cond), Ulf Wallin & Meri Englund (violin)<BR> +## CD DG 429 413-2: Chamber Orchestra of Europe, Gidon Kremer (cond & violin), Tatiana Grindenko (violin)<BR> +## CD DG 445 520-2: Chamber Orchestra of Europe, Gidon Kremer (cond & violin), Tatiana Grindenko (violin)<BR> +## CD Nonesuch 7559-79633-2: Kremeratica Baltica, Gidon Kremer (violin)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: My Memories Take Me To You +Title-Opus: 123 +Title-Dates: 1977 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Adventures of Travka +Title-Opus: 124 +Title-Dates: 1977 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Life-Story of an Unknown Actor +Title-Opus: 125 +Title-Dates: 1977 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/ocd606.htm">Olympia OCD 606</A>: USSR Cinematography SO, Emin Khachaturian (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cap71041.htm">Capriccio hybrid CD/SACD CC 71041</A>: Berlin Radio Symphony Orchestra, Frank Strobel (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Ascent +Title-Opus: 126 +Title-Dates: 1977 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cap71061.htm">Capriccio 71061</A> (SACD): Rundfunk-Sinfonieorchester Berlin, Frank Strobel (cond)</I></DL> + +Title-Name: In Memoriam +Title-For: symphony orchestra +Title-Opus: 127 +Title-Dates: 1972-1978 + +## <DL><DD>Orchestral version of the piano quintet<BR> +## 1. Moderato (attacca) - 7 min.<BR> +## 2. Tempo di Valse - 6 min.<BR> +## 3. Andante (attacca) - 7 min.<BR> +## 4. Lento - 4 min. 30 sec.<BR> +## 4. Moderato pastorale - 3 min. 30 sec.<P> +## <I>CD BIS CD 447: Malmö SO, Lev Markiz (cond)<BR> +## CD Chandos CHAN 9466: Russian State Symphony Orchestra, Valery Polyansky (cond)<BR> +## CD Sony Classical CD 48241: London SO, Seiji Ozawa (cond)</I></DL> + +Title-Type: Concerto +Title-No: 3 +Title-For: violin and chamber orchestra +Title-Opus: 128 +Title-Dates: 1978 + +## <DL><DD>In three movements:<BR> +## 1. Moderato - 10 min.<BR> +## 2. Agitato (attacca) - 6 min.<BR> +## 3. Moderato - 11 min.<P> +## <I>LP Eurodisc 201 234: Berlin Philharmonic Chamber Ensemble, W. Nelson (cond), Gidon Kremer (violin)<BR> +## LP Melodiya C10 15681 000: Soloists Ensemble, Y. Nikolayevsky (cond), Oleg Kagan (violin) +## CD BIS CD 517: Malmö SO, Eri Klas (cond), Oleg Krysa (violin)<BR> +## CD Ondine ODE 893-2: Virtuosi di Kuhmo, Sibelius Academy Wind Players, Ralf Gothoni (cond), Mark Lubotsky (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/phoenix150.htm">Phoenix PHCD 150</A>: Moscow Tchaikovsky SO, Mikhail Kukushkin (cond), Levon Ambartsumian (violin)<BR> +## CD Teldec 4509-94540-2: Chamber Orchestra of Europe, Gidon Kremer (violin)</I></DL> + +Title-Type: Sonata +Title-No: 1 +Title-For: cello and piano +Title-Opus: 129 +Title-Dates: 1978 + +## <DL><DD>In three movements:<BR> +## 1. Largo (attacca) - 4 min.<BR> +## 2. Presto (attacca) - 7 min.<BR> +## 3. Largo - 12 min.<P> +## <I>CD Aeon AECD 0636: Marc Coppey (cello), Peter Laul (piano)<BR> +## CD BIS CD 336: Torleif Thedéen (cello), Roland Pöntinen (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bbm11032.htm">Black Box BBM 1032</A>: Raphael Wallfisch (cello), John York (piano)<BR> +## CD Chandos CHAN 9705: Alexander Ivashkin (cello), Irina Schnittke (piano)<BR> +## CD <A HREF="/ovar/shosrev/cda67534.htm">Hyperion CDA 67534</A>: Alban Gerhardt (cello), Steven Osborne (piano)<BR> +## CD Globe GLO 5041: D. Ferschtmann (cello), M. Baslavskaya (piano)<BR> +## CD Harmonia Mundi HMN 91 1628: Xavier Phillips (cello), Huseyin Sermet (piano)<BR> +## CD Marco Polo 8.223334: Maria Kliegel (cello), Raimund Havenith (piano)<BR> +## CD Melodiya SUCD 10 00088: T. Hugh (cello), C. Tourocque (piano)<BR> +## CD Ode Manu CDMANU 1480: Alexander Ivashkin (cello), Tamas Vesmas (piano)<BR> +## CD <A HREF="/ovar/sovrev/kancheli/qtz2032.htm">Quartz QTZ 2032</A>: Matthew Barley (cello), Stephen De Pledge (piano)<BR> +## CD Thorofon CTH 2459: Sonja Schröder (cello), Peter Martin (piano)<BR> +## CD Unicorn Kanchana DKP CD 9083: Alexander Baillie (cello), Piers Lane (piano)<BR> +## CD United Recordings 88006-2: Paul Marleyn (cello), Sarah Morley (piano)</I></DL> + +Title-RAW: "Silent Night" ("Stille Nacht") arranged +Title-For: violin and piano +Title-Opus: 130 +Title-Dates: 1978 + +## <DL><DD>On motives of the like-named German Christmas carol.<BR> +## Lento (one movement).<BR> +## Employing the melody by Franz Xaver Gruber.<BR> +## Duration: 5 minutes<P> +## <I>CD ASV CDDCA 877: Mateja Marinkovic (violin), Linn Hendry (piano)<BR> +## CD BIS CD 527: U. Wallin (violin), Roland Pöntinen (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bbm1025.htm">Black Box BBM 1025</A>: Roman Mints (violin), Evgenia Chudinovich(piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/ni5631.htm">Nimbus NI 5631</A>: Daniel Hope (violin), Simon Mulligan (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)<BR> +## CD Teldec 4509-94540-2: Gidon Kremer (violin), Christoph Eschenbach (piano)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Father Sergei +Title-Opus: 131 +Title-Dates: 1978 + +Title-Type: Incidental music +Title-Related-How: to Alexander Vampilov's play +Title-Related-Name: A Duck Shooting Party +Title-Opus: 132 +Title-Dates: 1978 + +Title-Type: Incidental music +Title-Related-How: to Nikolai Gogol's play +Title-Related-Name: The Dead Soul Register (The Census List) +Title-Opus: 133 +Title-Dates: 1978 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/chan9885.htm">Chandos CHAN 9885</A>: Russian State SO, Valeri Polyansky (cond), Lev Butenin (reciter)</I></DL> + +Title-RAW: Hymn IV +Title-For: cello, bassoon, double-bass, harpsichord, harp, timpani and bells +Title-Opus: 134 +Title-Dates: 1974-1979 + +## <DL><DD>Duration: 3 minutes.<P> +## <I>LP Melodiya C10 28753 008: USSR Bolshoi Theatre Soloists Ensemble, A. Lazarev (cond)<BR> +## CD BIS CD 507: Torleif Thédéen (cello), Christian Davidsson (bassoon), Entcho Radoukanov (double-bass), Mayumi Kamata (harpsichord), Ingegerd Fredlund (harp), Anders Holdar & Anders Loguin (timpani & bells)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Torleif Thédéen (cello), Christian Davidsson (bassoon), Entcho Radoukanov (double-bass), Mayumi Kamata (harpsichord), Ingegerd Fredlund (harp), Anders Holdar & Anders Loguin (timpani & bells)<BR> +## CD Melodiya SUCD 10 00061: USSR Bolshoi Theatre Soloists Ensemble, A. Lazarev (cond)</I></DL> + +Title-Type: Symphony +Title-No: 2 +Title-Name: St. Florian +Title-For: vocal soloists, chamber choir and symphony orchestra +Title-Opus: 135 +Title-Dates: 1979 + +## <DL><DD>In six movements:<BR> +## 1. Recitativo (Kyrie) - 11 min.<BR> +## 2. Maestoso (Gloria) - 5 min.<BR> +## 3. Moderato (Credo) - 9 min.<BR> +## 4. Pesante (Cucifixus) - 8 min. 30 sec.<BR> +## Coda: Agitato (Et resurrexit), Maestoso - 4 min.<BR> +## Introduction to V. Andante (Sanctus) - 4 min.<BR> +## 5. Andante (Sanctus. Benedictus) - 8 min.<BR> +## 6. Andante (Agnus Dei) - 11 min.<P> +## <I>LP Melodiya C10 23085 (2 LP-set): Leningrad PO, USSR Ministry of Culture Chamber Chorus, Gennadi Rozhdestvensky (cond)<BR> +## CD BBC Radio Classics 15656-9196-2: BBC Symphony Chorus and Orchestra, BBC Singers, Gennadi Rozhdestvensky (cond), Jean Temperley (contralto), +## Paul Esswood (alto), Neil Jenkins (tenor), Jonathan Robarts (bass)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis667.htm">BIS CD 667</A>: Royal Stockholm PO, Mikaeli Chamber Choir, Leif Segerstam (cond), Mikael Bellini (alto), +## Malena Ernman (mezzo-soprano), Goran Eliasson (tenor), Torkel Borelius (bass)<BR> +## CD Chandos CHAN 9519: Russian State Symphonic Cappella and SO, Valery Polyansky (cond), +## Marina Katsman contralto), Yaroslav Zdorov (alto), Oleg Dolgov (tenor), Sergei Veprintsev (bass)<BR> +## CD Melodiya SUCD 10 00063: Leningrad PO, USSR Ministry of Culture Chamber Chorus, Gennadi Rozhdestvensky (cond)</I></DL> + +Title-Type: Concerto +Title-For: piano and string orchestra +Title-In-Movements: in a single movement +Title-Opus: 136 +Title-Dates: 1979 + +## <DL><DD>Duration: 21 minutes.<P> +## <I>LP Melodiya C10 22845 004: Lithuanian Chamber Orchestra, S. Sondeckis (cond), V. Krainev (cond)<BR> +## CD BIS CD 377: New Stockholm Chamber Orchestra, Lev Markiz (cond), Roland Pontinen (piano)<BR> +## CD <A HREF="/ovar/shosrev/brioso109.htm">Brioso BR 109</A>: Moscow PO, Vassily Sinaisky (cond), O. Volkov (piano)<BR> +## CD Capriccio 67 016: Moscow Virtuosi, V. Spivakov (cond)<BR> +## CD CHAN 9564: Russian State SO, Valery Polyansky (cond), Igor Khudolei (piano)<BR> +## CD <A HREF="/ovar/shosrev/delos3259.htm">Delos DE 3259</A>: Moscow Chamber Orchestra, Constantine Orbelian (pianist and conductor)<BR> +## CD Erato 2292-45742-2: London Sinfonietta, Gennadi Rozhdestvensky (cond), Victoria Postnikova (piano)<BR> +## CD Koch International Classics 37159-2: Moscow PO, Donald Barra (cond), Israela Margalit (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cdman175.htm">Manchester Classical Gallery CDMAN 175</A>: St. Petersburg Mozarteum Chamber Orchestra, Arcady Shteinlukht (cond), Veronica Reznikovskaya (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/mv06.htm">MV Productions SPPS CD06</A>: Omsk Chamber Orchestra, Yuri Nikolaevsky (cond), Svetlana Ponomarëva (piano)<BR> +## CD Ondine ODE 893-2: Virtuosi di Kuhmo, Ralf Gothoni (cond & piano)<BR> +## CD RCA Victor Red Seal 09026- 60466-2: Moscow Virtuosi, Vladimir Spivakov (cond), Vladimir Krainev (piano)<BR> +## CD RCA Victor 74321-24894-2 (2 CD-set): Moscow Virtuosi, Vladimir Spivakov (cond), Vladimir Krainev (piano)<BR> +## CD Teldec 2292-45742-2: London Sinfonietta, Gennadi Rozhdestvensky (cond), Victoria Postnikova (piano)</I></DL> + +Title-RAW: Polyphonic Tango +Title-For: ensemble +Title-Opus: 137 +Title-Dates: 1979 + +## <DL><DD><I>Live Rec: Bolshoi Theatre Soloists Ensemble, A. Lazarev (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cpo999804.htm">CPO 999 804-2</A>: Radio-Philharmonie Hannover des NDR, Eiji Oue (cond)</I></DL> + +Title-RAW: Peaceful Music (Stille Musik) +Title-For: violin and cello +Title-Opus: 138 +Title-Dates: 1979 + +## <DL><DD>Lento (one movement).<BR> +## Duration: 6 minutes.<P> +## <I>CD ASV CDDCA 877: Mateja Marinkovic (violin), Timothy Hugh (cello)<BR> +## CD BIS CD 697: Oleg Krysa (violin), Torleif Thédéen (cello)<BR> +## CD Marco Polo 8.223334: Burkhard Godhoff (violin), Maria Kliegel (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554728.htm">Naxos 8.554728</A>: Mark Lubotsky & Dimity Hall (violin), Theodore Kuchar & Irini Morozova (viola), Alexander Ivashkin & Julian Smiles(cello), Irina Schnittke (piano)</I></DL> + +Title-RAW: In Memoriam Igor Strawinsky, Sergei Prokofiev and Dmitri Shostakovich +Title-For: piano six hands +Title-Opus: 139 +Title-Dates: 1979 + +## <DL><DD>Based on Chinese March from "The nightingale", Humoresque Scherzo and Polka from "The age of gold".<P> +## <I>Live Rec: Robert Nasveld, Polo de Haas & Nico de Vente (piano)<BR> +## Live Rec: Pianokwadraat (piano)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Paradoxes of the Evolution +Title-Opus: 140 +Title-Dates: 1979 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Fantasies of Faryatyev +Title-Opus: 141 +Title-Dates: 1979 + +Title-Name: Passacaglia +Title-For: large symphony orchestra +Title-Opus: 142 +Title-Dates: 1979-1980 + +## <DL><DD>Duration: 19 minutes.<P> +## <I>CD BIS 437: Malmö SO, Leif Segerstam (cond)</I></DL> + +Title-RAW: Gogol Suite +Title-For: orchestra +Title-Related-How: from the spectacle +Title-Related-Name: The Tale of the Inspector +Title-Opus: 143 +Title-Dates: 1980 + +## <DL><DD>Suite from the music to a production of "The Dead Souls Register".<BR> +## Orchestral version by Gennadi Rozhdestvensky.<BR> +## In eight movements:<BR> +## 1. Overture - 1 min.<BR> +## 2. Chichikov's Childhood - 2 min. 30 sec.<BR> +## 3. The Portrait - 7 min.<BR> +## 4. The Overcoat - 2 min. 30 sec.<BR> +## 5. Ferdinand VIII - 1 min. 30 sec.<BR> +## 6. The Bureaucrats - 2 min. 30 sec.<BR> +## 7. The Ball - 6 min. 30 sec.<BR> +## 8. The Legacy - 6 min.<P> +## <I>LP Melodiya C10 18761-62: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)<BR> +## CD BIS CD 557: Malmö SO, Lev Markiz (cond)<BR> +## CD Pope Music PM 1007-2: Russian SO, Mark Gorenstein (cond)</I></DL> + +Title-RAW: "The Revisionist's Tale", transcription of five movements of the Gogol Suite +Title-For: two pianos +Title-Opus: 143a + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/sp22566.htm">Sonora Products SO 22566 CD</A>: Natalia Zusman & Inna Heifetz (pianos)</I></DL> + +Title-RAW: Three Madrigals for soprano, violin, viola, double-bass, vibraphone and harpsichord on poems by Francisco Tanzer +Title-Opus: 144 +Title-Dates: 1980 + +## <DL><DD>1. Sur une étoile (Andante)<BR> +## 2. Entfernung (Moderato)<BR> +## 3. Reflection (Andante)<P> +## <I>LP Melodiya C10 18403-4: Bolshoi Theatre Soloists Ensemble, A. Lazarev (cond), N. Lee (soprano)<BR> +## CD Hyperion CDA 66885: Capricorn, , Timothy Mason (cond), Sarah Leonard (soprano)</I></DL> + +Title-RAW: Three Scenes for soprano and ensemble without text +Title-Opus: 145 +Title-Dates: 1980 + +## <DL><DD>Dedicated to Mark Pekarski and his ensemble.<BR> +## 1. Poco pesante<BR> +## 2. Moderato<BR> +## 3. Andante</DL> + +Title-RAW: Moz-Art, version +Title-For: ensemble +Title-Opus: 146 +Title-Dates: 1980 + +## <DL><DD>For oboe, harp, harpsichord, violin, cello and double bass.<BR> +## Allegretto (one movement)</DL> + +Title-Name: Minnesang +Title-For: 52 voices +Title-Opus: 147 +Title-Dates: 1980-1981 + +## <DL><DD>Duration: 19 minutes.<P> +## <I>CD Chandos CHAN 9126: Danish National Radio Choir, Stefan Parkman Cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cda67297.htm">Hyperion CDA 67297</A>: Holst Singers, Stephen Layton (cond)</I></DL> + +Title-Type: String Quartet +Title-No: 2 +Title-Opus: 148 +Title-Dates: 1980 + +## <DL><DD>Commissioned by Universal Edition, Vienna.<BR> +## In four movements:<BR> +## 1. Moderato (attacca) - 3 min.<BR> +## 2. Agitato (attacca) - 6 min.<BR> +## 3. Mesto (attacca) - 6 min.<BR> +## 4. Moderato - 7 min.<P> +## <I>LP Panton 81 0752: Stamic Quartet<BR> +## CD Arabesque Z 6707: Lark Quartet<BR> +## CD BIS CD 467: Tale Quartet<BR> +## CD Claves CD 50-9504/5 (2 CD-set): (Arrangement by Rachlevsky) Kremlin Chamber Orchestra, Misha Rachlevsky (cond)<BR> +## CD Collins Classics 1450-2: Duke Quartet<BR> +## CD Gramavision GV 79439-2: Arditti Quartet<BR> +## CD Nonesuch 79500-2: Kronos Quartet<BR> +## CD Nonesuch 7559-79504-2 (10 CD-set): Kronos Quartet</I></DL> + +Title-RAW: Two Short Pieces +Title-For: organ +Title-Opus: 149 +Title-Dates: 1980 + +## <DL><DD><I>CD Melodiya SUCD 10 00009: O. Yanchenko (organ)<BR> +## CD Melodiya SUCD 10 00066: O. Yanchenko (organ)<BR> +## CD Aurophon AU 31840 <BR> +## CD Precosa-Aulos PRE 66 022 AUL: F. Herz (organ)</I></DL> + +Title-RAW: Three Cadenzas to Mozart's Piano Concerto in C major KV 467 +Title-Opus: 150 +Title-Dates: 1980 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Plane Crew +Title-Opus: 151 +Title-Dates: 1980 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Larisa +Title-Opus: 152 +Title-Dates: 1980 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Little Tragedies +Title-Opus: 153 +Title-Dates: 1980 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Farewell +Title-Opus: 154 +Title-Dates: composed with Artyomov, 1980 + +Title-Type: Symphony +Title-No: 3 +Title-Opus: 155 +Title-Dates: 1981 + +## <DL><DD>In four movements:<BR> +## 1. Moderato<BR> +## 2. Allegro<BR> +## 3. Allegro pesante (attacca)<BR> +## 4. Adagio<P> +## <I>LP Melodiya C10 25175: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)<BR> +## CD BIS CD 477: Stockholm PO, Eri Klas (cond)<BR> +## CD Melodiya SUCD 10 00064: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Eugene Onegin +Title-Opus: 156 +Title-Dates: 1981 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: What Does Babirussya Need Tusks For? +Title-Opus: 157 +Title-Dates: 1981 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: I Am With You Again +Title-Opus: 158 +Title-Dates: 1981 + +Title-Type: Concerto Grosso +Title-No: 2 +Title-For: violin, cello and triple symphony orchestra +Title-Opus: 159 +Title-Dates: 1981-1982 + +## <DL><DD>In four movements:<BR> +## 1. Andantino. Allegro - 6 min.<BR> +## 2. Pesante - 9 min.<BR> +## 3. Allegro - 5 min. 30 sec.<BR> +## 4. Andantino - 12 min.<P> +## <I>LP Melodiya A10 00509 005: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)<BR> +## CD BIS CD 567: Malmö SO, Lev Markiz (cond), Oleg Krysa (violin), Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan10180.htm">Chandos CHAN 10180</A>: Russian State SO, Valeri Polyansky (cond), Tatiana Grindenko (violin), Alexander Ivashkin (cello)<BR> +## CD Melodiya SUCD 10-00068: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond)</I></DL> + +Title-Type: Septet +Title-For: flute, two clarinets, violin, viola, cello and harpsichord or organ +Title-Opus: 160 +Title-Dates: 1981-1982 + +## <DL><DD>Introduction. Moderato (attacca)<BR> +## 1. Perpetuum mobile. Allegretto<BR> +## 2. Choral. Moderato<P> +## <I>CD Chandos CHAN 9466: Bolshoi Theatre Orchestra Soloists Ensemble, Valery Polyansky (cond)</I></DL> + +Title-RAW: "Course of life" ("Lebenslauf") +Title-For: four metronomes, piano and three percussionists +Title-Opus: 161 +Title-Dates: 1982 + +## <DL><DD>Dedicated to Wilfried Brennecke and John Cage.</DL> + +Title-RAW: A Paganini +Title-For: violin solo +Title-Opus: 162 +Title-Dates: 1982 + +## <DL><DD>Andante (one movement).<BR> +## Duration: 11 minutes.<P> +## <I>LP DG 415 484-1: Gidon Kremer (violin)<BR> +## CD ASV CDDCA 877: Mateja Marinkovic (violin)<BR> +## CD BIS CD 697: Oleg Krysa (violin)<BR> +## CD BIS CD 1051: Ilya Gringolts (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bridge9104.htm">Bridge 9104</A>: Joanna Kurkowicz (violin)<BR> +## CD DG 415 484-2: Gidon Kremer (violin)<BR> +## CD DG 445 520-2: Gidon Kremer (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/phoenix150.htm">Phoenix PHCD 150</A>: Levon Ambartsumian (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/sp22579.htm">Sonora Products SO 22579 CD</A>: Valery Gradow (violin)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Sturdy Boy +Title-Opus: 163 +Title-Dates: 1982 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Autumn +Title-Opus: 164 +Title-Dates: 1982 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Pencil and Eraser +Title-Opus: 165 +Title-Dates: 1982 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Meteoric Shower/Star Fall +Title-Opus: 166 +Title-Dates: 1982 + +Title-Name: Seid Nüchtern und Wachet +Title-Type-After-Name: cantata +Title-For: soloists (counter-tenor, counter-alto, tenor and bass), mixed chorus and orchestra +Title-Opus: 167 +Title-Dates: 1983 + +## <DL><DD>Text from "The History of D. Johann Faustus".<BR> +## Parts:<BR> +## 1. Folget nun - 3 min.<BR> +## 2. Die vierundzwanzig Jahre - 2 min.<BR> +## 3. Gehen also miteinander - 2 min.<BR> +## 4. Meine liebe - 5 min.<BR> +## 5. Ach, mein Herr Fauste - 5 min.<BR> +## 6. Doktor Faustus klagte - 3 min.<BR> +## 7. Es geschah - 5 min.<BR> +## 8. Diese gemeldete Magistri - 3 min.<BR> +## 9. Also endet sich - 4 min.<BR> +## 10. Seid nüchtern und wachet - 3 min.<P> +## <I>CD BIS 437: Malmö SO & Chorus, James DePreist (cond), Inger Blom (mezzo-soprano), Mikael Bellini (alto), Louis Devos (tenor), Ulrik Cold (bass)</I></DL> +## <B>Opus 168: String Quartet no. 3 (1983)</B><BR> +## <DL><DD>Commissioned by Society for New Music, Mannheim.<BR> +## In three movements:<BR> +## 1. Andante - 5 min.<BR> +## 2. Agitato - 7 min.<BR> +## 3. Pesante - 7 min.<P> +## <I>CD Arabesque Z 6707: Lark Quartet<BR> +## CD <A HREF="/ovar/sovrev/schnittke/arco54.htm">Arco Diva UP 0054-2 131</A>: Kapralova Quartet<BR> +## CD BIS CD 467: Tale Quartet<BR> +## CD Etcetera KTC 1124: Mondriaan Quartet<BR> +## CD LDR CD1008: Britten Quartet<BR> +## CD Virgin Classics VC 7 91436-2: Borodin Quartet<BR> +## CD Nonesuch 79500-2: Kronos Quartet</I></DL> + +Title-RAW: Cadenza to Mozart's Piano Concerto in C major KV 503 (first movement) +Title-Opus: 169 +Title-Dates: 1983 + +Title-RAW: Two Cadenzas to Mozart's Bassoon Concerto in B flat major KV 191 +Title-Opus: 170 +Title-Dates: 1983 + +Title-Name: Sound and Echo (Schall und Hall) +Title-For: trombone and organ +Title-Opus: 171 +Title-Dates: 1983 + +## <DL><DD>Lento (one movement).<BR> +## Duration: 12 minutes.<P> +## <I>CD BIS CD 488: Christian Lindberg (trombone), Gunnar Idenstam (organ)<BR> +## CD Chandos CHAN 9466: Anatoly Skobelev (trombone), Ludmilla Golub (organ)</I></DL> + +Title-RAW: "A Streetcar Named Desire (Endstation Sehnsucht)", ballet in two acts by John Neumeier after the play by Tennessee Williams (from Symphony No. 1) +Title-Opus: 172 +Title-Dates: 1983 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Leave-Taking +Title-Opus: 173 +Title-Dates: 1983 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: A Fairy Tale of Travels +Title-Opus: 174 +Title-Dates: 1983 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Darling of the Audience +Title-Opus: 175 +Title-Dates: 1983 + +Title-Type: Symphony +Title-No: 4 +Title-For: two vocal soloists, piano, chamber chorus and chamber orchestra +Title-In-Movements: in a single movement +Title-Opus: 176 +Title-Dates: 1984 + +## <DL><DD>Duration: 41 minutes.<P> +## <I>LP Melodiya A10 00271 005: USSR Ministry of Culture Orchestra SO, Gennadi Rozhdestvensky (cond), Viktoria Postnikova (piano) +## CD BIS CD 497: Stockholm Sinfonietta, Uppsala Academic Chamber Choir, Okko Kamu (cond), Mikael Bellini (alto)<BR> +## CD Chandos CHAN 9463: Russian State SO, Russian State Symphonic Cappella, Valery Polyansky (cond), Iaroslav Zdorov (alto), Dmitri Pianov (tenor)<BR> +## CD Melodiya SUCD 10-00065: USSR Ministry of Culture Orchestra SO, Gennadi Rozhdestvensky (cond), Viktoria Postnikova (cond) (Recording: 1986)</I></DL> + +Title-Type: Concerto +Title-No: 4 +Title-For: violin and orchestra +Title-Opus: 177 +Title-Dates: 1984 + +## <DL><DD>In four movements:<BR> +## 1. Andante (attacca) - 5 min.<BR> +## 2. Vivo (attacca) - 7 min.<BR> +## 3. Adagio (attacca) - 9 min. 30 sec.<BR> +## 4. Lento - 12 min. 30 sec.<P> +## <I>CD BIS CD517: Malmö SO, Eri Klas (cond), Oleg Krysa (violin)<BR> +## CD Teldec 4509-98440-2: Members of Philharmonia Orchestra, Gidon Kremer (violin)</I></DL> + +Title-RAW: Transcription of Scott Joplin's Ragtime +Title-For: orchestra +Title-Opus: 178 +Title-Dates: 1984 + +Title-RAW: Transcription of Adolf Jensen's Serenada +Title-For: mezzo-soprano and orchestra +Title-Opus: 179 +Title-Dates: 1984 + +Title-RAW: Transcription of Friedrich Nietsche's Incantation +Title-For: mezzo-soprano and orchestra +Title-Opus: 180 +Title-Dates: 1984 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The White Poodle +Title-Opus: 181 +Title-Dates: 1984 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Dead Souls +Title-Opus: 182 +Title-Dates: 1984 + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Dead Souls +Title-For: orchestra +Title-Opus: 182a + +## <DL><DD><I>Live Rec: Royal Concertgebouw Orchestra, Gennadi Rozhdestvensky (cond), Viktoria Postnikova (piano)</I></DL> + +Title-Name: Ritual +Title-For: large symphony orchestra +Title-Opus: 183 +Title-Dates: 1984-1985 + +## <DL><DD>In memory of the victims of the Second World War (on occasion of the 40th Anniversary of the liberation of Belgrade). +## Moderato (one movement). +## Duration: 9 minutes.<P> +## <I>CD BIS 437: Malmö SO, Leif Segerstam (cond)</I></DL> + +Title-Type: Concerto +Title-For: soprano and mixed chorus +Title-Opus: 184 +Title-Dates: 1984-1985 + +## <DL><DD>On verses from the "Book of Mournful Songs" by Gregory of Narek.<BR> +## Commissioned by Moscow Chamber Choir.<BR> +## Part One - 15 min.<BR> +## Part Two - 7 min. 30 sec.<BR> +## Part Three - 13 min.<BR> +## Part Four - 4 min. 30 sec.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1157.htm">BIS 1157</A>: Swedish Radio Choir, Tonu Kaljuste (cond)<BR> +## CD Chandos CHAN 9126: Danish National Radio Choir, Stefan Parkman Cond)<BR> +## CD Chandos CHAN 9332: Russian State Symphonic Cappella, Valery Polyansky (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/cda67297.htm">Hyperion CDA 67297</A>: Holst Singers, Stephen Layton (cond)<BR> +## CD Melodiya SUCD 10 00066: USSR Ministry of Culture Chamber Chorus, Valery Polyansky (cond), E. Dof-Donskaya (soprano)<BR> +## CD Nonesuch 79500-2: Kronos Quartet (arrangement)</I></DL> + +Title-RAW: Othello, ballet in two acts by John Neumeier after the tragedy by William Shakespeare +Title-Opus: 185 +Title-Dates: 1985 + +Title-RAW: "Sketches", choreographic phantasy on a theme by Nikolai Gogol, ballet +Title-In-Movements: in one movement +Title-Opus: 186 +Title-Dates: 1985 + +## <DL><DD><I>CD CdM Russian Season RUS 288 155: Bolshoi Theatre Orchestra, Andrey Chistiakov (cond)</I></DL> + +Title-RAW: "(K)ein Sommernachtstraum", subtitled +Title-Name: Not after Shakespeare +Title-For: symphony orchestra +Title-Opus: 187 +Title-Dates: 1985 + +## <DL><DD>Duration: 11 minutes.<P> +## <I>CD BIS 437: Malmö SO, Leif Segerstam (cond)<BR> +## CD Chandos CHAN 9722: Russian State SO, Valery Polyansky (cond)<BR> +## CD Pope Music Pm 1007-2: Russian SO, Mark Gorenstein (cond)</I></DL> + +Title-Type: Concerto Grosso +Title-No: 3 +Title-For: two violins, harpsichord, celesta, piano and fourteen strings +Title-Opus: 188 +Title-Dates: 1985 + +## <DL><DD>In five movements:<BR> +## 1. Allegro - 2 min.<BR> +## 2. Risoluto - 3 min.<BR> +## 3. Pesante - 8 min.<BR> +## 4. Adagio - 8 min.<BR> +## 5. Moderato - 3 min.<P> +## <I>CD Decca 430 698-2: Royal Concertgebouw Orchestra, Riccardo Chailly (cond), R. Brautigam (harpsichord/piano), Viktor Lieberman & Jaap van Zweden (violin)<BR> +## CD London 430 698-2: Royal Concertgebouw Orchestra, Riccardo Chailly (cond), R. Brautigam (harpsichord/piano), Viktor Lieberman & Jaap van Zweden (violin)<BR> +## CD BIS CD 537: Stockholm Chamber Orchestra, Lev Markiz (cond), Patrik Swedrup & Tale Olsson (violin)</I></DL> + +Title-Type: Concerto +Title-For: viola and orchestra +Title-Opus: 189 +Title-Dates: 1985 + +## <DL><DD>In three movements:<BR> +## 1. Largo - 5 min.<BR> +## 2. Allegro molto - 12 min.<BR> +## 3. Largo - 17 min.<P> +## <I>LP Melodiya A10 00499 007: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond), Yuri Bashmet (viola)<BR> +## CD BIS CD 447: Malmö SO, Lev Markiz (cond), Nobuko Imai (viola)<BR> +## CD EMI CDC 5 55107-2: Jerusalem SO, David Shallon (cond), Tabea Zimmermann (viola)<BR> +## CD Koch Schwann 31523-2: Philharmonia Orchestra, Heinrich Schiff (cond), Isabelle van Keulen (viola)<BR> +## CD Melodiya SUCD 10-00068: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond), Yuri Bashmet (viola)<BR> +## CD Regis RRC 1141: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond), Yuri Bashmet (viola)<BR> +## CD RCA Victor Red Seal RD 60446: London SO, Mstislav Rostropovich (cond), Yuri Bashmet (viola)<BR> +## CD RCA Victor 74321-24894-2 (2 CD-set): London SO, Mstislav Rostropovich (cond), Yuri Bashmet (viola)<BR> +## CD <A HREF="/ovar/sovrev/kancheli/ecm1471.htm">ECM New Series 1471</A> (437 199-2): Saarbrücken Radio SO, Dennis Russell Davies (cond), Kim Kashkashian (viola)<BR> +## DVD TDK DV-VPOVG: Vienna Philharmonic Orchestra, Valery Gergiev (cond), Yuri Bashmet (viola) (Live recording, 2000)</I></DL> + +Title-RAW: Music to an Imagined Play +Title-For: ensemble +Title-Opus: 190 +Title-Dates: 1985 + +## <DL><DD>In three movements:<BR> +## 1. Winter Road<BR> +## 2. Budding Song<BR> +## 3. March<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/ocd606.htm">Olympia OCD 606</A>: USSR Cinematography SO, Emin Khachaturian (cond)</I></DL> + +Title-Type: String Trio +Title-Opus: 191 +Title-Dates: 1985 + +## <DL><DD>Commissioned by the Alban Berg Society in commemoration of the composer's 100th Anniversary.<BR> +## In two movements:<BR> +## 1. Moderato - 12 min. 30 sec.<BR> +## 2. Adagio - 13 min.<P> +## <I>CD ASV CDDCA 868: Mateja Marinkovic (violin), Paul Silverthorne (viola), Timothy Hugh (cello)<BR> +## CD BIS CD 547: Patrik Swedrup (violin), Ingegerd Kierkegaard (viola), Helena Nilsson (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/emi55627.htm">EMI CDC 5 55627 2</A>: Gidon Kremer (violin), Yuri Bashmet (viola), M. Rostropovich (cello)<BR> +## CD Hyperion CDA 66885: Capricorn<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554728.htm">Naxos 8.554728</A>: Mark Lubotsky & Dimity Hall (violin), Theodore Kuchar & Irini Morozova (viola), Alexander Ivashkin & Julian Smiles(cello), Irina Schnittke (piano)</I></DL> + +Title-RAW: Transcription of Alban Berg's Canon, arrangement +Title-For: nine strings +Title-Opus: 192 +Title-Dates: 1985 + +Title-RAW: Transcription of Alban Berg's Canon, arrangement +Title-For: violin and chamberorchestra +Title-Opus: 192a +Title-Dates: 1987 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/emi55627.htm">EMI CDC 5 55627 2</A>: Moscow Soloists, Gidon Kremer (violin)</I></DL> + +Title-Type: Concerto +Title-No: 1 +Title-For: cello and orchestra +Title-Opus: 193 +Title-Dates: 1985-1986 + +## <DL><DD>In four movements:<BR> +## 1. Pesante: Moderato - 14 min.<BR> +## 2. Largo - 10 min.<BR> +## 3. Allegro vivace - 4 min.<BR> +## 4. Largo - 12 min.<P> +## <I>CD BIS CD 507: Danish National Radio SO, Leif Segerstam (cond), Torleif Thédéen (cello)<BR> +## CD BIS CD 1507: Danish National Radio SO, Leif Segerstam (cond), Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Danish National Radio SO, Leif Segerstam (cond), Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan9852.htm">Chandos CHAN 9852</A>: Russian State Symphony Orchestra, Valery Polyansky (cond), Alexander Ivashkin (cello)<BR> +## CD EMI CDC 7 54443-2: London PO, Kurt Masur (cond), Natalia Gutman (cello)<BR> +## CD Marco Polo 8.223334: Saarbrucken Radio SO, Gerhard Markson (cond), Maria Kliegel (cello)<BR> +## CD Regis RRC 1141: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond), Natalia Gutman (cello)<BR> +## CD Sikorski SIK 7-003E: USSR Ministry of Culture SO, Gennadi Rozhdestvensky (cond), Natalia Gutman (cello)</I></DL> + +Title-RAW: "Peer Gynt", ballet in three acts by John Neumeier based on Henrik Ibsen's drama +Title-Opus: 194 +Title-Dates: 1986 + +## <DL><DD>Parts:<BR> +## Prologue<BR> +## 1. Into the World - 4 min.<BR> +## <I>Act I: Norway</I><BR> +## 2. Peer and his mother Åse - 2 min.<BR> +## 3. Peer´s Imagination - 4 min.<BR> +## 4. Peer at Ingrid´s wedding celebration - 2 min.<BR> +## 5. Appearance of Solveig and her parents - 4 min.<BR> +## 6. Pas de deux: Solveig-Peer - 4 min.<BR> +## 7. The World of the small-minded Locals - 1 min.<BR> +## 8. In the mountains with Ingrid - 5 min.<BR> +## 9. The Troll-world - 5 min.<BR> +## 10. The Böyg - 3 min. 30 sec.<BR> +## 11. Peer´s Solitude - 1 min. 30 sec.<BR> +## 12. Solveig comes to Peer (pas de deux) - 6 min. 30 sec.<BR> +## 13. The Woman in Green - 2 min. 30 sec.<BR> +## 14. Åse´s Death (pas de deux) - 5 min. 30 sec.<BR> +## <I>Act II: Out in the World - Illusions</I><BR> +## 15. Overture - 3 min. 30 sec.<BR> +## 16. Auditions - 4 min.<BR> +## 17. Rainbow Sextet - 3 min.<BR> +## 18. Peer as Slavedealer - 1 min.<BR> +## 19. Scene and Opening Night Party - 4 min. 30 sec.<BR> +## 20. Emperor of the World - 2 min. 30 sec.<BR> +## 21. Peer´s dance with the whip - 2 min.<BR> +## 22. Solveig´s dance - 2 min. 30 sec.<BR> +## 23. Peer´s mad dance - 2 min. 30 sec.<BR> +## 24. Peer´s coronation - 1 min.<BR> +## 25. Finale - 2 min.<BR> +## <I>Act III: Return</I><BR> +## 26. Mesto - 2 min.<BR> +## 27. Andante - 4 min.<BR> +## 28. Peer´s memories - 2 min.<BR> +## 29. Ingrid´s burial - 3 min.<BR> +## 30. Scene with Solveig - 6 min. 30 sec.<BR> +## 31. Peer surrounded by his aspects - 1 min.<BR> +## 32. Song of the World - 30 sec.<BR> +## 33. The Onion - 3 min.<BR> +## 34. Despair and escape - 40 sec.<BR> +## 35. Deliverance - 2 min.<BR> +## <I>Epilogue:</I><BR> +## 36. Out of the world - 24 min.<BR> +## 37. Appendix - 2 min.<P> +## <I>CD BIS CD 677/8: Stockholm Royal Opera Orchestra, Eri Klas (cond)</I></DL> + +Title-Type: Epilogue +Title-Related-How: from +Title-Related-Name: Peer Gynt +Title-For: symphony orchestra and tape (mixed chorus) +Title-Opus: 194a +Title-Dates: 1987 + +## <DL><DD><I>Live Rec: Oslo PO, Mariss Yansons (cond)</I></DL> + +Title-Type: Epilogue +Title-Related-How: from +Title-Related-Name: Peer Gynt +Title-For: cello, piano and tape +Title-Opus: 194b +Title-Dates: 1993 + +## <DL><DD><I>CD Chandos CHAN 9705: Alexander Ivashkin (cello), Irina Schnittke (piano)</I></DL> + +Title-Type: Trio Sonata +Title-For: chamber orchestra +Title-Opus: 195 +Title-Dates: 1987 + +## <DL><DD>Arrangement after the String Trio (1985).<BR> +## 1. Moderato - 18 min.<BR> +## 2. Adagio - 16 min.<P> +## <I>CD BIS CD 537: Stockholm Chamber Orchestra, Lev Markiz (cond)<BR> +## CD Col legno WWE 8CD20041 (9 CD-set)<BR> +## CD ECM New Series 453-512-2: Stuttgart Chamber Orchestra, Dennis Russell Davies (cond)<BR> +## CD RCA Victor Red Seal RD 60446: Moscow Soloists, Yuri Bashmet (cond)</I></DL> + +Title-Name: Quasi Una Sonata +Title-For: violin and chamber orchestra +Title-Opus: 196 +Title-Dates: 1987 + +## <DL><DD>Arrangement of Violin Sonata no. 2 (1967).<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1437.htm">BIS CD 1437</A>: Tapiola Sinfonietta, Ralf Gothoni (cond), Ulf Wallin (violin)<BR> +## CD DG 429 413-2: Chamber Orchestra of Europe, Gidon Kremer (cond & violin), Yuri Smirnov (piano)<BR> +## CD DG 445 520-2: Chamber Orchestra of Europe, Gidon Kremer (cond & violin), Yuri Smirnov (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/dg471626.htm">DG 471 626-2</A>: Chamber Orchestra of Europe, Gidon Kremer (violin)<BR> +## CD Sony Classical SK 53 271: English Chamber Orchestra, Mstislav Rostropovich (cond), Mark Lubotsky (violin)</I></DL> + +Title-Type: Piano Sonata +Title-No: 1 +Title-Opus: 197 +Title-Dates: 1987-1988 + +## <DL><DD>Dedicated to Vladimir Feltsman.<BR> +## 1. Lento<BR> +## 2. Allegretto<BR> +## 3. Lento<BR> +## 4. Allegro<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bc17292.htm">Berlin Classics 0017292BC</A>: Ragna Schirmer (piano)<BR> +## CD Chandos CHAN 8962: Boris Berman (piano)<BR> +## CD Consonance 81-0009: Vasily Lobanov (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/mv06.htm">MV Productions SPPS CD06</A>: Svetlana Ponomareva (piano)</I></DL> + +Title-Count: Four +Title-Type: Aphorisms +Title-For: chamber orchestra +Title-Opus: 198 +Title-Dates: 1988 + +## <DL><DD>In four movements:<BR> +## 1. Lento<BR> +## 2. Moderato<BR> +## 3. Allegretto<BR> +## 4. Lento<P> +## <I>LP Melodiya: Soloists Ensemble of Bolshoi Theatre Orchestra, A. Lazarev (cond)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Balcony +Title-Opus: 199 +Title-Dates: 1988 + +Title-RAW: Symphony No. 5 / Concerto Grosso +Title-No: 4 +Title-Opus: 200 +Title-Dates: 1988 + +## <DL><DD>In four movements:<BR> +## 1. Allegro - 5 min.<BR> +## 2. Allegretto - 9 min.<BR> +## 3. Lento (attacca). Allegro - 17 min.<BR> +## 4. Lento - 9 min.<P> +## <I>CD BIS CD 427: Gothenburg SO, Neeme Järvi (cond)<BR> +## CD Decca 430 698-2: Royal Concertgebouw Orchestra, Riccardo Chailly (cond)<BR> +## CD London 430 698-2: Royal Concertgebouw Orchestra, Riccardo Chailly (cond)</I></DL> + +Title-Type: Concerto +Title-For: piano four hands and chamber orchestra +Title-In-Movements: in a single movement +Title-Opus: 201 +Title-Dates: 1988 + +## <DL><DD>Dedicated to Viktoria Postnikova and Irina Schnittke.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/cpo999804.htm">CPO 999 804-2</A>: Radio-Philharmonie Hannover des NDR, Eiji Oue (cond), Genova and Dimitrov Piano Duo<BR> +## CD Erato 2292-45742-2: London Sinfonietta, Gennadi Rozhdestvensky (cond), Irina Schnittke & Victoria Postnikova (piano)<BR> +## CD Revelation RV 10009: Gennadi Rozhdestvensky (cond), Irina Schnittke & Victoria Postnikova (piano)</I></DL> + +Title-RAW: Three Verses of Viktor Schnittke +Title-For: tenor and piano +Title-Opus: 202 +Title-Dates: 1988 + +## <DL><DD>1. Wer Gedichte macht, ...<BR> +## 2. Der Geiger<BR> +## 3. Dein Schweigen</DL> + +Title-RAW: "Twelve Penitential Psalms", verses of repentance +Title-For: mixed chorus a cappella +Title-Opus: 203 +Title-Dates: 1988 + +## <DL><DD>In twelve movements><P> +## <I>CD Chandos CHAN 9480: Danish National Radio Choir, Stefan Parkman (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/ecm4535132.htm">ECM 453-513-2 (1583)</A>: Swedish Radio Choir, Tonu Kaljuste (cond)</I></DL> + +Title-RAW: Piano Quartet in A minor (after Mahler) +Title-Opus: 204 +Title-Dates: 1988 + +## <DL><DD>Dedicated to Oleg Krysa.<BR> +## Quoting a fragment for piano quartet by the 16-year old Gustav Mahler.<BR> +## Allegro (one movement). +## Duration: 6 minutes.<P> +## <I>CD BIS CD 547: Tale Olsson (violin), Ingegerd Kierkegaard (viola), Helena Nilsson (cello), Roland Pöntinen (piano)<BR> +## CD Virgin Classics VC 7 91436-2: Borodin Quartet, Ludmilla Berlinsky (piano)</I></DL> + +Title-Name: Sounding Letters (Klingende Buchstaben) +Title-For: solo cello +Title-Opus: 205 +Title-Dates: 1988 + +## <DL><DD>Dedicated to Alexander Ivashkin on the occasion of his 40th birthday.<BR> +## Andantino (one movement)<BR> +## Duration: 4 minutes.<P> +## <I>CD BIS CD 507: Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis300507.htm">BIS CD 300507</A>: Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/shosrev/cda67534.htm">Hyperion CDA 67534</A>: Alban Gerhardt (cello)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/naxos8554728.htm">Naxos 8.554728</A>: Alexander Ivashkin (cello)<BR> +## CD Ode Manu CDMANU 1480: Alexander Ivashkin (cello)</I></DL> + +Title-Type: Monologue +Title-For: viola and chamberorchestra +Title-Opus: 206 +Title-Dates: 1989 + +## <DL><DD>Largo (one movement).<P> +## <I>CD EMI CDC 5 55107-2: Jerusalem SO, David Shallon (cond), Tabea Zimmermann (viola)<BR> +## CD Melodiya SUCD 10 00492: Moscow Soloists, Yuri Bashmet (cond & viola)<BR> +## CD RCA Victor RD 60464: Moscow Soloists, Yuri Bashmet (cond & viola)<BR> +## CD RCA Victor 74321-24894-2: Moscow Soloists, Yuri Bashmet (cond & viola)</I></DL> + +Title-RAW: Eröffnungsvers zum Ersten Festspielsonntag +Title-For: mixed chorus and organ +Title-Opus: 207 +Title-Dates: 1989 + +## <DL><DD>Ihr Völker alle, klatscht in die Hände... (from Psalm 47)</DL> + +Title-Name: 3 x 7 +Title-For: seven instruments +Title-Opus: 208 +Title-Dates: 1989 + +## <DL><DD>For clarinet, horn, trombone, harpsichord, violin, violoncello and double bass.<BR> +## Moderato (one movement)</DL> + +Title-Type: String Quartet +Title-No: 4 +Title-Opus: 209 +Title-Dates: 1989 + +## <DL><DD>Commissioned by Vienna Concert Hall Society.<BR> +## 1. Lento<BR> +## 2. Allegro<BR> +## 3. Lento<BR> +## 4. Vivace<BR> +## 5. Lento<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/arco54.htm">Arco Diva UP 0054-2 131</A>: Kapralova Quartet<BR> +## CD EMI CDC 7 54660-2 : Alban Berg Quartet<BR> +## CD EMI CMS 5 65765-2 (4 CD-set): Alban Berg Quartet<BR> +## CD Nonesuch 79500-2: Kronos Quartet<BR> +## CD Nonesuch 7559-79504-2 (10 CD-set): Kronos Quartet</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Visitor of a Museum +Title-Opus: 210 +Title-Dates: 1989 + +Title-Type: Concerto +Title-No: 2 +Title-For: cello and orchestra +Title-Opus: 211 +Title-Dates: 1990 + +## <DL><DD>Dedicated to Mstislav Rostropovich.<BR> +## 1. Moderato (attaca) - 4 min.<BR> +## 2. Allegro (attaca) - 10 min.<BR> +## 3. Lento (attaca) - 10 min.<BR> +## 4. Allegretto vivo (attaca) - 5 min.<BR> +## 5. Grave - 16 min.<P> +## <I>CD BIS CD 567: Malmö SO, Lev Markiz (cond), Torleif Thédéen (cello)<BR> +## CD Chandos CHAN 9722: Russian State SO, Valery Polyansky (cond), Alexander Ivashkin (cello)<BR> +## CD Sony Classical CD 48241: London SO, Seiji Ozawa (cond), Mstislav Rostropovich (cello)</I></DL> + +Title-RAW: Moz-Art à la Mozart +Title-For: harp and eight flutes +Title-Opus: 212 +Title-Dates: 1990 + +Title-RAW: Madrigal in Memoriam Oleg Kagan +Title-For: violin or solo +Title-Opus: 213 +Title-Dates: 1990 + +## <DL><DD>Lento (one movement).<BR> +## Duration: 10 minutes.<P> +## <I>CD ASV CDDCA 877: Mateja Marinkovic (violin)<BR> +## CD BIS CD 697: Oleg Krysa (violin)</I></DL> + +Title-RAW: Madrigal in Memoriam Oleg Kagan +Title-For: cello solo +Title-Opus: 213a +Title-Dates: 1990 + +## <DL><DD>Lento (one movement)<BR> +## Duration: 12 minutes.<P> +## <I>CD BIS CD 697: Torleif Thédéen (cello)<BR> +## CD <A HREF="/ovar/shosrev/cda67534.htm">Hyperion CDA 67534</A>: Alban Gerhardt (cello)<BR> +## CD Ode Manu CDMANU 1480: Alexander Ivashkin (cello)</I></DL> + +Title-Count: Three +Title-Type: Fragments +Title-For: harpsichord +Title-Opus: 214 +Title-Dates: 1990 + +## <DL><DD>1. Andante<BR> +## 2. Vivo<BR> +## 3. Lento</DL> + +Title-Count: Five +Title-Type: Aphorisms +Title-For: piano +Title-Opus: 215 +Title-Dates: 1990 + +## <DL><DD>Dedicated to Joseph Brodsky and Alexander Slobodyanik.<BR> +## 1. Moderato assai<BR> +## 2. Allegretto<BR> +## 3. Lento<BR> +## 4. Senza tempo<BR> +## 5. Grave<P> +## <I>CD Chandos CHAN 9704: Boris Berman (piano)<BR> +## CD Ode Manu CDMANU 1480: Tamas Vesmas (piano)<BR> +## CD Thorofon CTH 2459: Peter Martin (piano)</I></DL> + +Title-Type: Piano Sonata +Title-No: 2 +Title-Opus: 216 +Title-Dates: 1990 + +## <DL><DD>Dedicated to Irina Schnittke.<BR> +## 1. Moderato<BR> +## 2. Lento<BR> +## 3. Allegro moderato<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bc17292.htm">Berlin Classics 0017292BC</A>: Ragna Schirmer (piano)<BR> +## CD Chandos CHAN 9704: Boris Berman (piano)<BR> +## CD Sony Classical SK 53 271: Irina Schnittke (piano)</I></DL> + +Title-RAW: Cadenzas to Mozart's Piano Concerto in B flat major KV 39 +Title-Opus: 217 +Title-Dates: 1990 + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: Russia: Love For This Country +Title-Opus: 218 +Title-Dates: 1990 + +Title-RAW: Life with an Idiot, opera in 2 acts (4 scenes) by V. Yerofeyev after his likenamed short story +Title-Opus: 219 +Title-Dates: 1990-1991 + +## <DL><DD><I>Sony S2K 52 495: Rotterdam PO, Vocal Ensemble, Mstislav Rostropovich (cond), Dale Duesing (baritone), +## Romain Bischkoff (bass), Teresa Ringholz (soprano), Howard Haskin (tenor), Leonid Zimnenko +## (bass), Robin Leggate (tenor)</I></DL> + +Title-Name: Sutartines +Title-For: percussion, organ and string orchestra +Title-Opus: 220 +Title-Dates: 1991 + +Title-RAW: Suite in Old Style +Title-For: chamberorchestra +Title-Opus: 221 +Title-Dates: 1991 + +## <DL><DD>Arrangement of opus 51 by Vladimir Spivakov and Vladimir Milman.<BR> +## 1. Pastorale<BR> +## 2. Ballet<BR> +## 3. Minuet<BR> +## 4. Fugue<BR> +## 5. Pantomime<P> +## <I>LP Melodiya A10 00483 007: USSR Bolshoi Theatre Soloists Ensemble, Gennadi Rozhdestvensky (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis1437.htm">BIS CD 1437</A>: Tapiola Sinfonietta, Ralf Gothoni (cond)<BR> +## CD Capriccio 67 016: Moscow Virtuosi, V. Spivakov (cond)<BR> +## CD Consonance 81-0009: Igor Bogulavsky (viola d’amore), Viktor Grislin (vibraphone), Alla Litvienko (harpsichord), Viktor Gabinsky (marimba), Vadim Vasilykov (bells)<BR> +## CD Melodiya SUCD 10 00010: (parts 1 & 2) USSR Bolshoi Theatre Soloists Ensemble, Igor Boguslavsky (viole d'amour)<BR> +## CD RCA Victor Red Seal RD 60370: Moscow Virtuosi, Vladimir Spivakov (cond)<BR> +## CD RCA Victor 74321-24894-2: Moscow Virtuosi, Vladimir Spivakov (cond)<BR> +## CD RCA Victor Artistes Repertoire 82876 502682: Moscow Virtuosi, Vladimir Spivakov (cond)</I></DL> + +Title-RAW: Festive Cantus +Title-For: violin, piano, mixed chorus and orchestra +Title-Opus: 222 +Title-Dates: 1991 + +## <DL><DD>Dedicated to Gennadi Rozhdestvensky on the occasion of his 60th birthday.</DL> + +Title-Type: Concerto Grosso +Title-No: 5 +Title-For: violin, piano and orchestra +Title-Opus: 223 +Title-Dates: 1991 + +## <DL><DD>Commissioned by the Carnegie Hall Corporation for the Cleveland Orchestra on the occasion of the Carnegie Hall Centenary.<BR> +## 1. Allegretto<BR> +## 2. -<BR> +## 3. Allegro vivace<P> +## <I>CD DG 437 091 2: Vienna PO, Christoph von Dohnanyi (cond), Gidon Kremer (violin), Rainer Keuschnig (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/dg471626.htm">DG 471 626-2</A>: Vienna PO, Christoph von Dohnanyi (cond), Gidon Kremer (violin), Rainer Keuschnig (piano)</I></DL> + +Title-RAW: For the 90th Birthday of Alfred Schlee +Title-For: viola solo +Title-Opus: 224 +Title-Dates: 1991 + +Title-Type: Symphony +Title-No: 6 +Title-Opus: 225 +Title-Dates: 1992 + +## <DL><DD>Dedicated to Mstislav Rostropovich and The National Symphony Orchestra of Washington.<BR> +## Commissioned by The National Symphony Orchestra of Washington, D. C., and Mstislav Rostropovich.<BR> +## 1. Allegro moderato - 14 min.<BR> +## 2. Presto - 4 min.<BR> +## 3. Adagio (attaca) - 10 min.<BR> +## 4. Allegro vivace - 5 min.<P> +## <I>CD BIS CD 747: BBC National Orchestra of Wales, Tadaaki Otaka (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan10180.htm">Chandos CHAN 10180</A>: Russian State SO, Valeri Polyansky (cond)</I></DL> + +Title-RAW: Agnus Dei +Title-For: two sopranos, female chorus and chamber orchestra +Title-Opus: 226 +Title-Dates: 1992 + +## <DL><DD>Part of the coope-rative work "Mass for Peace".<BR> +## Lento (one movement)</DL> + +Title-Type: Trio +Title-For: violin, cello and piano +Title-Opus: 227 +Title-Dates: 1992 + +## <DL><DD>Arrangement of the String Trio (1985).<BR> +## 1. Moderato - 14 min.<BR> +## 2. Adagio - 13 min.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/asv6251.htm">ASV Quicksilva CD QS 6251</A>: Barbican Piano Trio: Gaby Lester (violin), Robert Max (cello), James Kirby (piano)<BR> +## CD BIS CD 697: Oleg Krysa (violin), Torleif Thédéen (cello), Tatiana Tchekina (piano)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bbm1093.htm">Black Box BBM 1093</A>: Barbican Piano Trio: Gaby Lester (violin), Robert Max (cello), James Kirby (piano)<BR> +## CD Cascavelle VEL 3071: Trio Animae: Tomas Dratva (piano), Jean-Christophe Gawrysiak (violin), Dieter Hilpert (cello)<BR> +## CD Nimbus NI 5572: Vienna Piano Trio (Wolfgang Redik, violin; Marcus Trefny, cello; Stefan Mendl, piano)<BR> +## CD Sony Classical SK 53 271: Mark Lubotsky (violin), Mstislav Rostropovich (cello), Irina Schnittke (piano)</I></DL> + +Title-RAW: Musica Nostalgica +Title-For: cello and piano +Title-Opus: 228 +Title-Dates: 1992 + +## <DL><DD><I>CD Chandos CHAN 9705: Alexander Ivashkin (cello), Irina Schnittke (piano)<BR> +## CD <A HREF="/ovar/sovrev/kancheli/qtz2032.htm">Quartz QTZ 2032</A>: Matthew Barley (cello), Stephen De Pledge (piano)</I></DL> + +Title-Type: Piano Sonata +Title-No: 3 +Title-Opus: 229 +Title-Dates: 1992 + +## <DL><DD>1. Lento<BR> +## 2. Allegro<BR> +## 3. Largo<BR> +## 4. Allegro<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bc17292.htm">Berlin Classics 0017292BC</A>: Ragna Schirmer (piano)<BR> +## CD Chandos CHAN 9704: Boris Berman (piano)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Last Days of St. Petersburg +Title-Opus: 230 +Title-Dates: 1992 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cpo999796.htm">CPO 999 796-2</A>: Radio SO Berlin, Frank Strobel (cond)</I></DL> + +Title-RAW: Hommage to Dr. Zhivago, musical allegory +Title-Related-How: on motives of B. Pasternak's novel +Title-Related-Name: Doctor Zhivago +Title-Opus: 231 +Title-Dates: 1993 + +Title-RAW: "Gesualdo", opera in seven acts, a prologue and an epilogue by Richard Bletschacher +Title-Opus: 232 +Title-Dates: 1993 + +Title-RAW: Hommage to Grieg +Title-For: orchestra +Title-Opus: 233 +Title-Dates: 1993 + +## <DL><DD>Arrangement of a fragment from the ballet "Peer Gynt" for orchestra.<BR> +## Adagio (one movement)><P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/cpo999804.htm">CPO 999 804-2</A>: Radio-Philharmonie Hannover des NDR, Eiji Oue (cond)</I></DL> + +Title-Type: Symphony +Title-No: 7 +Title-Opus: 234 +Title-Dates: 1993 + +## <DL><DD>Dedicated to Kurt Masur.<BR> +## Commissioned by the New York Philharmonic Orchestra.<BR> +## 1. Andante (attaca) - 5 min.<BR> +## 2. Largo - 2 min. 30 sec.<BR> +## 3. Allegro - 14 min.<P> +## <I>CD BIS CD 747: BBC National Orchestra of Wales, Tadaaki Otaka (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan9852.htm">Chandos CHAN 9852</A>: Russian State Symphony Orchestra, Valery Polyansky (cond)<BR></I></DL> + +Title-Type: Concerto Grosso +Title-No: 6 +Title-For: piano, violin and string orchestra +Title-Opus: 235 +Title-Dates: 1993 + +## <DL><DD><I>Antes Edition BM-CD31.9187: Bulgarian Orpheus Chamber Orchestra, Raitscho Christov (cond), Falko Steinbach (piano), Yosif Raduinov (violin)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/bis1437.htm">BIS CD 1437</A>: Tapiola Sinfonietta, Ralf Gothoni (cond & piano), Ulf Wallin (violin)<BR> +## CD Chandos CHAN 9359: Stockholm PO, Gennadi Rozhdestvensky (cond), Sasha Rozhdestvensky (violin), Viktoria Postnikova (piano)<BR> +## CD Nimbus NI 5582: English SO, William Boughton (cond), Daniel Hope (violin), Simon Mulligan (piano)</I></DL> + +Title-RAW: "Mother" for mezzo-soprano and piano after verses by Else Lasker-Schüler +Title-Opus: 236 +Title-Dates: 1993 + +## <DL><DD>Dedicated to Ulrich Eckhardt on occasion of his 60th birthday.<BR> +## Lento (one movement)</DL> + +Title-RAW: Improvisation +Title-For: cello solo +Title-Opus: 237 +Title-Dates: 1993 + +## <DL><DD>Commissioned by the 'Acanthes' contest, October 1994.<BR> +## Dedicated to Mstislav Rostropovich.<BR> +## Andante poco rubato (one movement)<P> +## <I>CD Ode Manu CDMANU 1480: Alexander Ivashkin (cello)<BR> +## CD Thorofon CTH 2459: Sonja Schröder (cello)</I></DL> + +Title-Type: Music +Title-Related-How: to the Film +Title-Related-Name: The Master and Margarita +Title-Opus: 238 +Title-Dates: 1993 + +## <DL><DD><I>CD <A HREF="/ovar/sovrev/schnittke/cpo999796.htm">CPO 999 796-2</A>: Radio SO Berlin, Frank Strobel (cond)</I></DL> + +Title-RAW: "The History of Dr. Johann Faustus", opera in three acts, a prologue and an epilogue by Jörg Morgener and Alfred Schnittke +Title-Opus: 239 +Title-Dates: 1994 + +## <DL><DD>Based on the likenamed book published by Johann Spies in 1587<P> +## <I>CD RCA Victor Red Seal 09026-68413-2: Hamburg State Opera Chorus and Orchestra, Gerd Albrecht (cond), +## Jurgen Freier (baritone), Eberhard Lorenz (tenor), Arno Raunig (alto), +## Hanna Schwarz (mezzo-soprano), Eberhard Buchner (tenor), Jonathan Barreto-Ramos (tenor), Christoph Johannes Wendel (baritone), Jurgen Fersch (bass)</I></DL> + +Title-Type: Symphonic Prelude +Title-Opus: 240 +Title-Dates: 1994 + +## <DL><DD>Commissioned by the Hamburg Philharmonic State Orchestra.<BR> +## Andante (one movement).<BR> +## Duration: 17 min. 30 sec.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1217.htm">BIS CD 1217</A>: Norrkoping Symphony Orchestra, Lü Jia (cond)</I></DL> + +Title-Type: Symphony +Title-No: 8 +Title-Opus: 241 +Title-Dates: 1994 + +## <DL><DD>Dedicated to Gennadi Rozhdestvensky and the Royal Stockholm Philharmonic Orchestra.<BR> +## Commissioned by Stockholm Concert Hall Foundation.<BR> +## 1. Moderato - 8 min. 30 sec.<BR> +## 2. Allegro moderato - 4 min. 30 sec.<BR> +## 3. Lento - 16 min.<BR> +## 4. Allegro moderato - 5 min.<BR> +## 5. Lento - 2 min.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1217.htm">BIS CD 1217</A>: Norrkoping Symphony Orchestra, Lü Jia (cond)<BR> +## CD Chandos CHAN 9359: Stockholm PO, Gennadi Rozhdestvensky (cond)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/chan9885.htm">Chandos CHAN 9885</A>: Russian State SO, Valeri Polyansky (cond)</I></DL> + +Title-Name: For Liverpool +Title-For: orchestra +Title-Opus: 242 +Title-Dates: 1994 + +## <DL><DD>Commissioned by The Royal Liverpool Philharmonic Society with funds provided by the Art Council of England.<BR> +## Duration: 12 min. 30 sec.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bis1217.htm">BIS CD 1217</A>: Norrkoping Symphony Orchestra, Lü Jia (cond)</I></DL> + +Title-Type: Triple concerto +Title-For: violin, viola, cello and string orchestra +Title-Name: Concerto for Three +Title-Opus: 243 +Title-Dates: 1994 + +## <DL><DD>1. Moderato - 6 min.<BR> +## 2. Larghetto - 4 min. 30 sec.<BR> +## 3. Largo - 4 min. 30 sec.<BR> +## 4. Attaca - Minuet - 5 min.<P> +## <I>CD BIS CD 1379/80 (2 CD-set): Toho Gakuen Orchestra, Koichiro Harada (cond), Yasushi Toyoshima (violin), Noboru Kamimura (cello), Nobuko Imai (viola)<BR> +## CD <A HREF="/ovar/sovrev/schnittke/emi55627.htm">EMI CDC 5 55627 2</A>: Moscow Soloists, Gidon Kremer (violin), Yuri Bashmet (viola), Mstislav Rostropovich (cello)<BR> +## CD Quartz QTZ 2052: West Kazakhstan Philharmonic Orchestra, Mikel Toms (cond), Roman Mints (violin), Maxim Rysanov (viola), Kristine Blaumane (cello)</I></DL> + +Title-RAW: Five Fragments to Pictures of Hieronymus Bosch +Title-For: tenor, violin, trombone, harpsichord, timpani and string orchestra +Title-Opus: 244 +Title-Dates: 1994 + +## <DL><DD>On texts by Aeshylus.<BR> +## Dedicated to Vladimir Spivakov.<BR> +## 1. Lento<BR> +## 2. Moderato<BR> +## 3. Andantino<BR> +## 4. Agitato<BR> +## 5. Senza tempo<P> +## <I>CD Capriccio 67 016: Moscow Virtuosi, V. Spivakov (cond)</I></DL> + +Title-Name: Lux Aeterna +Title-For: mixed chorus and orchestra +Title-Opus: 245 +Title-Dates: 1994 + +## <DL><DD>Orchestrated by Gennadi Rozhdestvensky.<BR> +## Commissioned by the International Bach Academy Stuttgart as part of the cooperative work 'Requiem of Reconciliation' for Europãisches Musikfest Stuttgart.<BR> +## Andante (one movement)</DL> + +Title-Type: Sonata +Title-No: 2 +Title-For: cello and piano +Title-Opus: 246 +Title-Dates: 1994 + +## <DL><DD>Dedicated to Mstislav Rostropovich.<BR> +## 1. Senza tempo<BR> +## 2. Allegro<BR> +## 3. Largo<BR> +## 4. Allegro<BR> +## 5. Lento<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/bbm11032.htm">Black Box BBM 1032</A>: Raphael Wallfisch (cello), John York (piano)<BR> +## CD Chandos CHAN 9705: Alexander Ivashkin (cello), Irina Schnittke (piano)<BR> +## CD <A HREF="/ovar/sovrev/kabalevsky/emi5720162.htm">EMI CZS 572016-2</A> (13 CD-set): Mstislav Rostropovich (cello), Aza Amintayeva (piano)<BR> +## CD Thorofon CTH 2459: Sonja Schröder (cello), Peter Martin (piano)</I></DL> + +Title-Type: Quartet +Title-For: four percussionists +Title-Opus: 247 +Title-Dates: 1994 + +## <DL><DD>Andante (one movement)</DL> + +Title-Type: Sonata +Title-No: 3 +Title-For: violin and piano +Title-Opus: 248 +Title-Dates: 1994 + +## <DL><DD>Dedicated to Mark Lubotsky.<BR> +## 1. Andante<BR> +## 2. Allegro (molto)<BR> +## 3. Adagio<BR> +## 4. Senza tempo<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/ni5631.htm">Nimbus NI 5631</A>: Daniel Hope (violin), Simon Mulligan (piano)<BR> +## CD Ondine ODE 893-2: Mark Lubotsky (violin), Irina Schnittke (piano)<BR> +## CD Stradivarius STR 33675: Francesco D'Orazio (violin), Giampaolo Nuti (piano)</I></DL> + +Title-Type: Minuet +Title-For: violin, viola and cello +Title-Opus: 249 +Title-Dates: 1994 + +## <DL><DD>Originally composed as an encore for the first performance of the Concerto for violin, viola and cello.<BR> +## Dedicated to Gidon Kremer, Yuri Bashmet and Mstislav Rostropovich.<P> +## <I>CD <A HREF="/ovar/sovrev/schnittke/emi55627.htm">EMI CDC 5 55627 2</A>: Gidon Kremer (violin), Yuri Bashmet (viola), Mstislav Rostropovich (cello)</I></DL> + +Title-Type: Sonatina +Title-For: piano four hands +Title-Opus: 250 +Title-Dates: 1995 + +## <DL><DD>Allegro moderato (one movement)</DL> + +Title-Type: Concerto +Title-For: viola and small orchestra +Title-Opus: 251 +Title-Dates: 1997 + +Title-Type: Variations +Title-For: string quartet +Title-Opus: 252 +Title-Dates: 1997 + +Title-Type: Symphony +Title-No: 9 +Title-Opus: 253 +Title-Dates: 1996-1998 + +## <DL><DD>Unfinished.</DL> +## <hr width=40% align=center size=3><P> diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/D_Shostakovich.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/D_Shostakovich.comp new file mode 100644 index 000000000..880a3403c --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/D_Shostakovich.comp @@ -0,0 +1,2569 @@ +# format = mail-header + + + + + +# opus_rex \bOp(?:us\b|\.)\s*\d+[a-i]?(?:[.,;\s]\s*No\.\s*\d+(?:\.\d+)*)? + +Title-Type: Scherzo +Title-Key: F sharp minor +Title-For: orchestra +Title-Opus: 1 +Title-Dates: 1919 + + + +Title-Count: Eight +Title-Type: Preludes +Title-For: piano +Title-Opus: 2 +Title-Dates: 1919-1920 + + + +Title-RAW: Minuet, Prelude and Intermezzo +Title-For: piano +Title-Opus: 2a +Title-Dates: 1919-1920 + + + +Title-RAW: Murzilka +Title-For: piano +Title-Opus: 2b +Title-Dates: 1920 + + + +Title-Count: Five +Title-Type: Preludes +Title-For: piano +Title-Opus: 2c +Title-Dates: 1920-1921 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: I Waited in the Grotto +Title-Related-By: by Rimsky-Korsakov +Title-For: soprano and orchestra +Title-Opus: 2d +Title-Dates: 1921 + + + +Title-Type: Theme and Variations +Title-Key: B flat major +Title-For: orchestra +Title-Opus: 3 +Title-Dates: 1921-1922 + + + +Title-Type: Transcription of Theme and Variations +Title-Key: B flat major +Title-For: solo piano +Title-Opus: 3a +Title-Dates: 1921-1922 + + + +Title-Name: Two Fables of Krilov +Title-For: mezzo-soprano, female chorus and chamber orchestra +Title-Opus: 4 +Title-Dates: 1922 + + + +Title-Type: Transcription +Title-Related-How: of +Title-Related-Name: Two Fables of Krilov +Title-For: mezzo-soprano and piano +Title-Opus: 4a +Title-Dates: 1922 + + + +Title-RAW: Three Fantastic Dances +Title-For: piano +Title-Opus: 5 +Title-Dates: 1922 + + + +Title-Type: Suite +Title-Key: F sharp minor +Title-For: two pianos +Title-Opus: 6 +Title-Dates: 1922 + + + +Title-Type: Scherzo +Title-Key: E flat major +Title-For: orchestra +Title-Opus: 7 +Title-Dates: 1923-1924 + + + +Title-Type: Transcription of Scherzo +Title-Key: E flat major +Title-For: solo piano +Title-Opus: 7a +Title-Dates: 1923-1924 + + + +Title-Type: Piano Trio +Title-No: 1 +Title-Key: C minor +Title-Opus: 8 +Title-Dates: 1923 + + + +Title-Count: Three +Title-Type: Pieces +Title-For: cello and piano +Title-Opus: 9 +Title-Dates: 1923-1924 + + + +Title-Type: Symphony +Title-No: 1 +Title-Key: F minor +Title-Opus: 10 +Title-Dates: 1924-1925 + + + +Title-Type: Prelude and Scherzo +Title-For: string octet/orchestra +Title-Opus: 11 +Title-Dates: 1924-1925 + + + +Title-Type: Piano Sonata +Title-No: 1 +Title-Opus: 12 +Title-Dates: 1926 + + + +Title-Name: Aphorisms +Title-Punct: , +Title-Count: ten +Title-Type: pieces +Title-For: piano +Title-Opus: 13 +Title-Dates: 1927 + + + +Title-Type: Symphony +Title-No: 2 +Title-Key: B flat major +Title-Name: To October +Title-RAW: with chorus +Title-Opus: 14 +Title-Dates: 1927 + + + +Title-Type: Reduction of the choral score +Title-Related-How: of +Title-Related-Name: Symphony No. 2 +Title-For: voices and piano +Title-Opus: 14a +Title-Dates: 1927 + + + +Title-Name: The Nose +Title-Type-After-Name: opera in three acts +Title-Related-By: after Gogol +Title-Opus: 15 +Title-Dates: 1927-1928 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Nose +Title-Punct: , +Title-For: tenor, baritone and orchestra +Title-Opus: 15a +Title-Dates: 1927-1928 + + + +Title-Type: Reduction of the accompaniment +Title-Related-How: of +Title-Related-Name: The Nose +Title-For: piano +Title-Opus: 15b +Title-Dates: 1927-1928 + + + +Title-Name: Tahiti-Trot +Title-For: orchestra +Title-Opus: 16 +Title-Dates: 1928 + + + +Title-RAW: Two Pieces by Scarlatti +Title-For: wind orchestra +Title-Opus: 17 +Title-Dates: 1928 + + + +Title-Type: Music +Title-Related-How: to the silent film +Title-Related-Name: New Babylon +Title-For: small orchestra +Title-Opus: 18 +Title-Dates: 1928-1929 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: New Babylon +Title-For: orchestra +Title-Opus: 18a +Title-Dates: 1976 + + + +Title-Type: Music +Title-Related-How: to the comedy +Title-Related-Name: The Bedbug +Title-Related-By: by Mayakovsky +Title-Opus: 19 +Title-Dates: 1929 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Bedbug +Title-For: orchestra +Title-Opus: 19a +Title-Dates: 1929 + + + +Title-Type: Arrangement of Music +Title-Related-How: to +Title-Related-Name: The Bedbug +Title-For: piano +Title-Opus: 19b +Title-Dates: 1929 + + + +Title-Type: Symphony +Title-No: 3 +Title-Key: E flat major +Title-Name: The First of May +Title-RAW: with chorus +Title-Opus: 20 +Title-Dates: 1929 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 3 +Title-RAW: for solo piano including a vocal score of the final chorus +Title-Opus: 20a +Title-Dates: 1929 + + + +Title-Name: Six Romances on Texts by Japanese Poets +Title-For: tenor and orchestra +Title-Opus: 21 +Title-Dates: 1928-1932 + + + +Title-Name: Six Romances on Texts by Japanese Poets +Title-For: tenor and piano +Title-Opus: 21a +Title-Dates: 1928-1932 + + + +Title-Name: The Age of Gold +Title-Type-After-Name: ballet in three acts +Title-Opus: 22 +Title-Dates: 1929-1930 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Age of Gold +Title-For: orchestra +Title-Opus: 22a +Title-Dates: 1929-1930 + + + +Title-Type: Polka +Title-Related-How: from +Title-Related-Name: The Age of Gold +Title-For: solo piano +Title-Opus: 22b +Title-Dates: 1935 + + + +Title-Type: Polka +Title-Related-How: from +Title-Related-Name: The Age of Gold +Title-For: piano four hands +Title-Opus: 22c +Title-Dates: 1962 + + + +Title-Count: Two +Title-Type: Pieces +Title-Related-How: for Erwin Dressel's Opera +Title-Related-Name: Armer Columbus +Title-For: orchestra +Title-Opus: 23 +Title-Dates: 1929 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: The Gunshot +Title-Related-By: by Bezymensky +Title-Opus: 24 +Title-Dates: 1929 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: Virgin Soil +Title-Related-By: by Gorbenko and L'vov +Title-Opus: 25 +Title-Dates: 1930 + + + +Title-Type: Transcription +Title-Related-How: of +Title-Related-Name: Symphony of Psalms +Title-Related-By: by Stravinsky +Title-For: two pianos +Title-Opus: 25a +Title-Dates: 1930 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Alone +Title-Punct: ( +Title-Name: Odna +Title-Punct: ) +Title-Opus: 26 +Title-Dates: 1930-1931 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Alone +Title-For: orchestra +Title-Opus: 26a +Title-Dates: 1930-1931 + + + +Title-Name: The Bolt +Title-Type-After-Name: ballet in three acts +Title-Opus: 27 +Title-Dates: 1930-1931 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Bolt +Title-For: orchestra +Title-Opus: 27a +Title-Dates: 1931 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: Rule, Britannia! +Title-Related-By: by Piotrovsky +Title-Opus: 28 +Title-Dates: 1931 + + + +Title-Name: Lady Macbeth of the Mtsensk District +Title-Type-After-Name: opera in four acts +Title-Related-By: after Leskov +Title-Opus: 29 +Title-Dates: 1930-1932 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Lady Macbeth of the Mtsensk District +Title-For: orchestra +Title-Opus: 29a +Title-Dates: 1930-1932 + + + +Title-RAW: Passacaglia from an Entr'acte +Title-Related-How: to +Title-Related-Name: Lady Macbeth of the Mtsensk District +Title-For: organ +Title-Opus: 29b +Title-Dates: 1930-1932 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Golden Mountains +Title-Opus: 30 +Title-Dates: 1931 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Golden Mountains +Title-For: orchestra +Title-Opus: 30a +Title-Dates: 1931 + + + +Title-Count: Two +Title-Type: Pieces +Title-For: string quartet +Title-Opus: 30b +Title-Dates: 1931 + + + +Title-Name: The Green Company +Title-Type-After-Name: overture +Title-Opus: 30c +Title-Dates: 1931 + + + +Title-Type: Music +Title-Related-How: to the stage revue +Title-Related-Name: Hypothetically Murdered +Title-Related-By: by Voyevodin and Riss +Title-Opus: 31 +Title-Dates: 1931 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Hypothetically Murdered +Title-Opus: 31a +Title-Dates: 1932 + + + +Title-RAW: Reduction of Four Movements of the Music +Title-Related-How: to +Title-Related-Name: Hypothetically Murdered +Title-For: piano +Title-Opus: 31b +Title-Dates: 1931 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: Hamlet +Title-Related-By: by Shakespeare +Title-Opus: 32 +Title-Dates: 1931-1932 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Hamlet +Title-For: small orchestra +Title-Opus: 32a +Title-Dates: 1932 + + + +Title-Name: From Karl Marx to Our Own Days +Title-Type-After-Name: symphonic poem +Title-For: solo voices, chorus and orchestra +Title-Opus: 32b +Title-Dates: 1932 + + + +Title-Name: The Big Lightning +Title-RAW: , unfinished comic opera +Title-Opus: 32c +Title-Dates: 1932 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Counterplan +Title-Opus: 33 +Title-Dates: 1932 + + + +Title-RAW: "Song about the Oncoming Train" and +Title-Name: My Heart's Aching and Moaning +Title-Related-How: from +Title-Related-Name: Counterplan +Title-For: voice and piano +Title-Opus: 33a +Title-Dates: 1956 + + + +Title-Name: We meet this Morning (The Song of the Young Workers) +Title-Related-How: from +Title-Related-Name: Counterplan +Title-For: voice and piano +Title-Opus: 33b +Title-Dates: 1956 + + + +Title-Count: Twenty-Four +Title-Type: Preludes +Title-For: piano +Title-Opus: 34 +Title-Dates: 1932-1933 + + + +Title-Type: Transcription of Twenty-Four Preludes +Title-For: violin and piano +Title-Opus: 34b +Title-Dates: 1932-1933 + + + +Title-Type: Transcription of Twenty-Four Preludes +Title-For: orchestra +Title-Opus: 34c +Title-Dates: 1932-1933 + + + +Title-RAW: Transcription of Prelude Opus 34 +Title-No: 14 +Title-For: orchestra +Title-Opus: 34d +Title-Dates: 1932-1933 + + + +Title-RAW: Concerto in C minor for piano, trumpet and strings, also known as Piano Concerto +Title-No: 1 +Title-Opus: 35 +Title-Dates: 1933 + + + +Title-Type: Reduction of Piano Concerto +Title-No: 1 +Title-For: two pianos +Title-Opus: 35a +Title-Dates: 1933 + + + +Title-Type: Music +Title-Related-How: to the animated film +Title-Related-Name: The Tale of the Priest and His Worker Balda +Title-For: chamber orchestra +Title-Opus: 36 +Title-Dates: 1933-1934 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Tale of the Priest and his Worker Balda +Title-Opus: 36a +Title-Dates: 1935 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: The Human Comedy +Title-Related-By: after Balzac +Title-For: small orchestra +Title-Opus: 37 +Title-Dates: 1933-1934 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Love and Hate +Title-Opus: 38 +Title-Dates: 1934 + + + +Title-Name: Suite for Jazz Orchestra No. 1 +Title-Opus: 38a +Title-Dates: 1934 + + + +Title-Name: The Limpid Stream +Title-Type-After-Name: ballet in three acts +Title-Opus: 39 +Title-Dates: 1934-1935 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Limpid Stream +Title-For: orchestra +Title-Opus: 39a +Title-Dates: 1934-1935 + + + +Title-Type: Moderato +Title-Related-How: from +Title-Related-Name: The Limpid Stream +Title-For: cello and piano +Title-Opus: 39b +Title-Dates: 1934-1935 + + + +Title-Type: Sonata +Title-Key: D minor +Title-For: cello and piano +Title-Opus: 40 +Title-Dates: 1934 + + + +Title-Type: Moderato +Title-For: cello and piano +Title-Opus: 40a +Title-Dates: 1934 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Youth of Maxim +Title-Opus: 41 +Title-Dates: 1934-1935 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Girl Friends +Title-Opus: 41a +Title-Dates: 1934-1935 + + + +Title-Count: Five +Title-Type: Fragments +Title-For: small orchestra +Title-Opus: 42 +Title-Dates: 1935 + + + +Title-Type: Symphony +Title-No: 4 +Title-Key: C minor +Title-Opus: 43 +Title-Dates: 1935-1936 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 4 +Title-For: two pianos +Title-Opus: 43a +Title-Dates: 1935-1936 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: Hail, Spain +Title-Related-By: by Afinogenov +Title-Opus: 44 +Title-Dates: 1936 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Return of Maxim +Title-Opus: 45 +Title-Dates: 1936-1937 + + + +Title-Name: Four Romances on Verses by Pushkin +Title-For: bass and piano +Title-Opus: 46 +Title-Dates: 1936-1937 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Four Romances on Verses by Pushkin +Title-For: bass and orchestra +Title-Opus: 46a +Title-Dates: 1936-1937 + + + +Title-RAW: Arrangement of Nos. 1, 2 and 3 +Title-Related-How: of +Title-Related-Name: Four Romances on Verses by Pushkin +Title-For: bass and string orchestra +Title-Opus: 46b +Title-Dates: 1936-1937 + + + +Title-Type: Symphony +Title-No: 5 +Title-Key: D minor +Title-Opus: 47 +Title-Dates: 1937 + + + +Title-RAW: Reduction of Scherzo (Allegretto) +Title-Related-How: of +Title-Related-Name: Symphony No. 5 +Title-For: solo piano +Title-Opus: 47a +Title-Dates: 1937 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Volochayev Days +Title-Opus: 48 +Title-Dates: 1936-1937 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Internationale +Title-Related-By: by Degeyter +Title-Opus: 48a +Title-Dates: 1937 + + + +Title-Name: The Twelve Chairs +Title-Type-After-Name: operetta +Title-Opus: 48b +Title-Dates: 1937-1938 + + + +Title-Type: String Quartet +Title-No: 1 +Title-Key: C major +Title-Opus: 49 +Title-Dates: 1938 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Vyborg District +Title-Opus: 50 +Title-Dates: 1938 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Maxim film-Trilogy +Title-For: orchestra and chorus +Title-Opus: 50a +Title-Dates: 1938 + + + +Title-Name: Suite for Jazz Orchestra No. 2 +Title-Opus: 50b +Title-Dates: 1938 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Friends +Title-Opus: 51 +Title-Dates: 1938 + + + +Title-RAW: Vocalise +Title-Related-How: from +Title-Related-Name: Friends +Title-RAW: for unaccompanied chorus +Title-Opus: 51a +Title-Dates: 1938 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Great Citizen +Title-RAW: , first part +Title-Opus: 52 +Title-Dates: 1938 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Man with a Gun +Title-Opus: 53 +Title-Dates: 1938 + + + +Title-Name: Lenin Symphony +Title-For: soli, chorus and orchestra +Title-Opus: 53a +Title-Dates: 1938-1939 + + + +Title-Type: Symphony +Title-No: 6 +Title-Key: B minor +Title-Opus: 54 +Title-Dates: 1939 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Great Citizen +Title-RAW: , second part +Title-Opus: 55 +Title-Dates: 1939 + + + +Title-Type: Music +Title-Related-How: to the animated film +Title-Related-Name: The Silly Little Mouse +Title-Opus: 56 +Title-Dates: 1939 + + + +Title-Name: Seven Finnish Folk Songs +Title-For: soprano, tenor and small orchestra +Title-Opus: 56a +Title-Dates: 1939 + + + +Title-Type: Piano Quintet +Title-Key: G minor +Title-Opus: 57 +Title-Dates: 1940 + + + +Title-Type: Orchestration +Title-Related-How: of the Opera +Title-Related-Name: Boris Godunov +Title-Related-By: by Mussorgsky +Title-Opus: 58 +Title-Dates: 1939-1940 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: King Lear +Title-Related-By: by Shakespeare +Title-Opus: 58a +Title-Dates: 1940 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: King Lear +Title-For: piano +Title-Opus: 58b +Title-Dates: 1940 + + + +Title-RAW: "Songs of the Fool" and +Title-Name: Ballad of Cordelia +Title-Related-How: from +Title-Related-Name: King Lear +Title-For: voice and piano +Title-Opus: 58c +Title-Dates: 1940 + + + +Title-RAW: Orchestration of "Wiener Blut" by Johann Strauss II +Title-Opus: 58d +Title-Dates: 1940 + + + +Title-RAW: Orchestration of "The Excursion Train Polka" by Johann Strauss II +Title-Opus: 58e +Title-Dates: 1940 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: 27 Romances and Songs Arrangements +Title-Opus: 58f +Title-Dates: 1941 + + + +Title-Name: The Oath to the People's Commissar +Title-For: bass, chorus and piano +Title-Opus: 58g +Title-Dates: 1941 + + + +Title-RAW: "Songs of a Guard's Division" ("The Fearless Regiments Are On the Move"), marching song for bass and mixed chorus with simple accompaniment for bayan or piano +Title-Opus: 58h +Title-Dates: 1941 + + + +Title-Type: Polka +Title-For: harp duet +Title-Key: F sharp minor +Title-Opus: 58i +Title-Dates: 1941 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Adventures of Korzinkina +Title-Opus: 59 +Title-Dates: 1940 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Adventures of Korzinkina +Title-Opus: 59a +Title-Dates: 1940 + + + +Title-Count: Three +Title-Type: Pieces +Title-For: solo violin +Title-Opus: 59b +Title-Dates: 1940 + + + +Title-Name: Katyusha Maslova +Title-Type-After-Name: opera +Title-Related-How: after Tolsty's novel +Title-Related-Name: Resurrection +Title-Opus: 59c +Title-Dates: 1940 + + + +Title-Type: Symphony +Title-No: 7 +Title-Key: C major +Title-Name: Leningrad +Title-Opus: 60 +Title-Dates: 1941 + + + +Title-Type: Piano Sonata +Title-No: 2 +Title-Key: B minor +Title-Opus: 61 +Title-Dates: 1943 + + + +Title-Name: Six Romances on Verses by English Poets +Title-For: bass and piano +Title-Opus: 62 +Title-Dates: 1942 + + + +Title-Name: Six Romances on Verses by English Poets +Title-For: bass and orchestra +Title-Opus: 62a +Title-Dates: 1943 + + + +Title-Type: Music +Title-Related-How: to the spectacle +Title-Related-Name: Native Country +Title-Type-After-Name: suite +Title-Name: Native Leningrad +Title-Opus: 63 +Title-Dates: 1942 + + + +Title-Type: Piece +Title-Related-How: of the Opera +Title-Related-Name: The Gamblers +Title-Related-By: after Gogol +Title-Opus: 63a +Title-Dates: 1941-1942 + + + +Title-Name: Solemn March +Title-For: military band/wind orchestra +Title-Opus: 63b +Title-Dates: 1942 + + + +Title-RAW: Patriotic Song +Title-Related-By: after Dolmatovsky +Title-For: voices +Title-Opus: 63c +Title-Dates: 1943 + + + +Title-Name: Song About the Red Army +Title-Related-By: after Golodny +Title-Opus: 63d +Title-Dates: 1943 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Eight British and American Folk Songs +Title-For: voice(s) and orchestra +Title-Opus: 63e +Title-Dates: 1943 + + + +Title-Name: Russian Folk Songs +Title-For: chorus +Title-Opus: 63f +Title-Dates: 1943 + + + +Title-Name: Three Russian Folk Songs +Title-RAW: for two soloists and chorus with piano accompaniment +Title-Opus: 63g +Title-Dates: 1943 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Zoya +Title-Opus: 64 +Title-Dates: 1944 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Zoya +Title-For: chorus and orchestra +Title-Opus: 64a +Title-Dates: 1944 + + + +Title-Name: She Was Born a Brave Girl in Her Homeland +Title-Related-How: from +Title-Related-Name: Zoya +Title-For: voice and piano +Title-Opus: 64b +Title-Dates: 1944 + + + +Title-Type: Symphony +Title-No: 8 +Title-Key: C minor +Title-Opus: 65 +Title-Dates: 1943 + + + +Title-Type: Music +Title-Related-How: to the spectacle +Title-Related-Name: Russian River +Title-For: soloists, choir and orchestra +Title-Opus: 66 +Title-Dates: 1944 + + + +Title-Type: Orchestration +Title-Related-How: of Fleishman's Chamber-Opera +Title-Related-Name: Rothschild's Violin +Title-Related-By: after Chekhov +Title-Opus: 66a +Title-Dates: 1944 + + + +Title-Type: Piano Trio +Title-No: 2 +Title-Key: E minor +Title-Opus: 67 +Title-Dates: 1944 + + + +Title-Type: String Quartet +Title-No: 2 +Title-Key: A major +Title-Opus: 68 +Title-Dates: 1944 + + + +Title-Name: Children's Notebook +Title-Punct: , +Title-Count: six +Title-Type: pieces +Title-For: piano +Title-Opus: 69 +Title-Dates: 1944-1945 + + + +Title-Type: Symphony +Title-No: 9 +Title-Key: E flat major +Title-Opus: 70 +Title-Dates: 1945 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 9 +Title-For: piano four hands +Title-Opus: 70a +Title-Dates: 1945 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Simple People +Title-Opus: 71 +Title-Dates: 1945 + + + +Title-Count: Two +Title-Type: Songs +Title-Related-How: to the spectacle +Title-Related-Name: Victorious Spring +Title-Related-By: after Svetlov +Title-For: voices and orchestra +Title-Opus: 72 +Title-Dates: 1945 + + + +Title-RAW: Accompaniment of Nos. 1 and 2 +Title-Related-How: of +Title-Related-Name: Victorious Spring +Title-RAW: , arranged +Title-For: piano +Title-Opus: 72a +Title-Dates: 1945 + + + +Title-Type: String Quartet +Title-No: 3 +Title-Key: F major +Title-Opus: 73 +Title-Dates: 1946 + + + +Title-Type: Transcription of String Quartet +Title-No: 3 +Title-For: strings and woodwinds +Title-Opus: 73a +Title-Dates: 1946 + + + +Title-Type: Reduction of String Quartet +Title-No: 3 +Title-For: two pianos +Title-Opus: 73b +Title-Dates: 1946 + + + +Title-Name: Poem of the Motherland +Title-Type-After-Name: cantata +Title-For: mezzosoprano, tenor, two baritones, chorus and orchestra +Title-Opus: 74 +Title-Dates: 1947 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Young Guards +Title-RAW: after Fadeyev's novel +Title-Opus: 75 +Title-Dates: 1947-1948 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Young Guards +Title-Opus: 75a +Title-Dates: 1951 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Pirogov +Title-Related-By: after German +Title-Opus: 76 +Title-Dates: 1947 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Pirogov +Title-For: orchestra +Title-Opus: 76a +Title-Dates: 1947 + + + +Title-Count: Three +Title-Type: Pieces +Title-For: orchestra +Title-Opus: 76b +Title-Dates: 1947-1948 + + + +Title-Type: Violin Concerto +Title-No: 1 +Title-Key: A minor +Title-Opus: 77 +Title-Dates: 1947-1948 + + + +Title-Type: Reduction of Violin Concerto +Title-No: 1 +Title-For: violin and piano +Title-Opus: 77a +Title-Dates: 1947-1948 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Michurin +Title-Opus: 78 +Title-Dates: 1948 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Michurin +Title-For: chorus and orchestra +Title-Opus: 78a +Title-Dates: 1964 + + + +Title-Name: Rayok +Title-RAW: (Little Paradise) +Title-For: four voices, chorus and piano +Title-Opus: 78b +Title-Dates: 1948 + + + +Title-Name: From Jewish Folk Poetry +Title-RAW: , song cycle +Title-For: soprano, contralto, tenor and piano +Title-Opus: 79 +Title-Dates: 1948 + + + +Title-Name: From Jewish Folk Poetry +Title-RAW: , song cycle +Title-For: soprano, contralto, tenor and small orchestra +Title-Opus: 79a +Title-Dates: 1948 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Meeting on the Elbe +Title-For: voices and piano +Title-Opus: 80 +Title-Dates: 1948 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Meeting on the Elbe +Title-For: voices and orchestra +Title-Opus: 80a +Title-Dates: 1948 + + + +Title-Count: Three +Title-Type: Songs +Title-Related-How: from +Title-Related-Name: Meeting on the Elbe +Title-For: voice and piano +Title-Opus: 80b +Title-Dates: 1956 + + + +Title-Name: Song of the Forests +Title-Type-After-Name: oratorio +Title-Related-By: after Dolmatovsky +Title-For: tenor, basssoli, mixed & boys' chorus and orchestra +Title-Opus: 81 +Title-Dates: 1949 + + + +Title-Name: In the Fields Stand the Collective Farms +Title-Related-How: from +Title-Related-Name: Song of the Forests +Title-For: children's chorus and mixed chorus +Title-Opus: 81a +Title-Dates: 1960 + + + +Title-Name: A Walk into the Future +Title-Related-How: from +Title-Related-Name: Song of the Forests +Title-For: voice and piano +Title-Opus: 81b +Title-Dates: 1962 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Fall of Berlin +Title-Opus: 82 +Title-Dates: 1949 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Fall of Berlin +Title-For: chorus and orchestra +Title-Opus: 82a +Title-Dates: 1950 + + + +Title-Name: Beautiful Day +Title-Related-How: from +Title-Related-Name: The Fall of Berlin +Title-RAW: , song for two-part children's chorus and piano +Title-Opus: 82b +Title-Dates: 1950 + + + +Title-Name: Vocalise +Title-Related-How: from +Title-Related-Name: The Fall of Berlin +Title-RAW: , song for s.a.t.b. chorus a cappella +Title-Opus: 82c +Title-Dates: 1950 + + + +Title-Type: String Quartet +Title-No: 4 +Title-Key: D major +Title-Opus: 83 +Title-Dates: 1949 + + + +Title-Type: Transcription of String Quartet +Title-No: 4 +Title-For: orchestra +Title-Opus: 83a +Title-Dates: 1949, arranged by Rudolf Barshai + + + +Title-Type: Reduction of String Quartet +Title-No: 4 +Title-For: two pianos four hands +Title-Opus: 83b +Title-Dates: 1949 + + + +Title-Name: Two Romances on Verses by Lermontov +Title-For: male voice and piano +Title-Opus: 84 +Title-Dates: 1950 + + + +Title-Name: Ballet Suite No. 1 +Title-For: orchestra +Title-Opus: 84a +Title-Dates: 1949 + + + +Title-RAW: Merry March +Title-For: two pianos +Title-Opus: 84b +Title-Dates: 1949 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Byelinsky +Title-For: orchestra and chorus +Title-Opus: 85 +Title-Dates: 1950 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Byelinsky +Title-For: chorus and orchestra +Title-Opus: 85a +Title-Dates: 1960, assembled by L. Atovmian + + + +Title-RAW: Four Choruses from the Music +Title-Related-How: to +Title-Related-Name: Byelinsky +Title-RAW: for s.a.t.b. chorus a cappella +Title-Opus: 85b +Title-Dates: 1950 + + + +Title-Name: Four Songs to Words by Dolmatovsky +Title-For: voice and piano +Title-Opus: 86 +Title-Dates: 1951 + + + +Title-Name: The Homeland Hears +Title-RAW: for chorus and tenor soloist with wordless chorus +Title-Opus: 86a +Title-Dates: 1951 + + + +Title-Name: Ten Russian Folk Song Arrangements +Title-For: soloists, mixed chorus and piano +Title-Opus: 86b +Title-Dates: 1951 + + + +Title-Count: Twenty-Four +Title-Type: Preludes and Fugues +Title-For: piano +Title-Opus: 87 +Title-Dates: 1950-1951 + + + +Title-RAW: Arrangement of No. 15 of Twenty-Four Preludes and Fugues +Title-For: two pianos +Title-Opus: 87a +Title-Dates: 1963 + + + +Title-RAW: Arrangement of No. 8 of Twenty-Four Preludes and Fugues +Title-For: orchestra +Title-Opus: 87b +Title-Dates: 1990 + + + +Title-Type: Transcription of Twenty-Four Preludes and Fugues +Title-For: violin and piano +Title-Opus: 87c +Title-Dates: 1990 + + + +Title-Name: Ten Poems on Texts by Revolutionary Poets +Title-For: chorus and boys' chorus a cappella +Title-Opus: 88 +Title-Dates: 1951 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Unforgettable Year 1919 +Title-Opus: 89 +Title-Dates: 1951 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Unforgettable Year 1919 +Title-For: orchestra +Title-Opus: 89a +Title-Dates: 1953, assembled by L. Atovmian + + + +Title-Name: Ballet Suite No. 2 +Title-For: orchestra +Title-Opus: 89b +Title-Dates: 1951, assembled by L. Atovmian + + + +Title-Count: Two +Title-Type: Pieces +Title-Related-How: from +Title-Related-Name: Ballet Suite No. 2 +Title-For: cello and piano +Title-Opus: 89c +Title-Dates: 1951 + + + +Title-Name: The Sun Shines on Our Motherland +Title-Type-After-Name: cantata +Title-Related-By: after Dolmatovsky +Title-For: mixed & boys' chorus and orchestra +Title-Opus: 90 +Title-Dates: 1952 + + + +Title-Type: Reduction of the Accompaniment +Title-Related-How: of +Title-Related-Name: The Sun Shines on Our Motherland +Title-For: piano +Title-Opus: 90a +Title-Dates: 1952 + + + +Title-Name: Four Monologues on Verses by Pushkin +Title-For: bass and piano +Title-Opus: 91 +Title-Dates: 1952 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Four Monologues on Verses by Pushkin +Title-For: bass and orchestra +Title-Opus: 91a +Title-Dates: 1952 + + + +Title-Name: Seven Doll's Dances +Title-For: piano +Title-Opus: 91b +Title-Dates: 1952 + + + +Title-Name: Ballet Suite No. 3 +Title-For: orchestra +Title-Opus: 91c +Title-Dates: 1952 + + + +Title-RAW: Greek Songs +Title-For: voice and piano +Title-Opus: 91d +Title-Dates: 1952-1953 + + + +Title-Name: Ballet Suite No. 4 +Title-For: orchestra +Title-Opus: 91e +Title-Dates: 1953 + + + +Title-Type: String Quartet +Title-No: 5 +Title-Key: B flat major +Title-Opus: 92 +Title-Dates: 1952 + + + +Title-Type: Symphony +Title-No: 10 +Title-Key: E minor +Title-Opus: 93 +Title-Dates: 1953 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 10 +Title-For: piano four hands +Title-Opus: 93a +Title-Dates: 1953 + + + +Title-RAW: Concertino +Title-For: two pianos +Title-Key: A minor +Title-Opus: 94 +Title-Dates: 1953 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Song of the Great Rivers +Title-Opus: 95 +Title-Dates: 1954 + + + +Title-Name: Poem of Labour +Title-Related-How: from +Title-Related-Name: Unity +Title-RAW: , arranged +Title-For: mixed chorus and orchestra +Title-Opus: 95a +Title-Dates: 1954 + + + +Title-Count: Two +Title-Type: Songs +Title-Related-How: from +Title-Related-Name: Unity +Title-RAW: ("A Song of Unity" and "Peaceful Labour"), arranged +Title-For: voice and piano +Title-Opus: 95b +Title-Dates: 1954 + + + +Title-Type: Waltz +Title-Related-How: from +Title-Related-Name: Unity +Title-For: orchestra +Title-Opus: 95c +Title-Dates: 1954 + + + +Title-Type: Music +Title-Related-How: to the play +Title-Related-Name: Hamlet +Title-Related-By: by Shakespeare +Title-Opus: 95d +Title-Dates: 1954 + + + +Title-RAW: "Pendozalis", Greek Song +Title-For: voice and piano +Title-Opus: 95e +Title-Dates: 1954 + + + +Title-Name: October Dawn +Title-Type-After-Name: song +Title-For: soloists and chorus +Title-Opus: 95f +Title-Dates: 1954 + + + +Title-Name: Festive Overture +Title-Key: A major +Title-For: orchestra +Title-Opus: 96 +Title-Dates: 1954 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The Gadfly +Title-RAW: , based on the novel by Voynich +Title-Opus: 97 +Title-Dates: 1955 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The Gadfly +Title-For: orchestra +Title-Opus: 97a +Title-Dates: 1955 + + + +Title-Name: Tarantella +Title-Related-How: from +Title-Related-Name: The Gadfly +Title-For: two pianos +Title-Opus: 97b +Title-Dates: 1955 + + + +Title-Count: Four +Title-Type: Waltzes +Title-For: flute, clarinet and piano +Title-Opus: 97c +Title-Dates: 1955 + + + +Title-RAW: Three Violin Duets for two violins with piano accompaniment +Title-Opus: 97d +Title-Dates: 1955 + + + +Title-RAW: Five Romances on Verses by Dolmatovsky +Title-For: bass and piano +Title-Opus: 98 +Title-Dates: 1954 + + + +Title-Name: There Were Kisses +Title-Type-After-Name: song +Title-Related-By: after Dolmatovsky +Title-For: voice and piano +Title-Opus: 98a +Title-Dates: 1954 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: The First Echelon +Title-Opus: 99 +Title-Dates: 1955-1956 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: The First Echelon +Title-For: chorus and orchestra +Title-Opus: 99a +Title-Dates: 1956 + + + +Title-RAW: Two Songs from the Music +Title-Related-How: to +Title-Related-Name: The First Echelon +Title-For: voice and piano +Title-Opus: 99b +Title-Dates: 1956 + + + +Title-RAW: Spanish Songs for (mezzo)soprano and piano +Title-Opus: 100 +Title-Dates: 1956 + + + +Title-Type: String Quartet +Title-No: 6 +Title-Key: G major +Title-Opus: 101 +Title-Dates: 1956 + + + +Title-Type: Piano Concerto +Title-No: 2 +Title-Key: F major +Title-Opus: 102 +Title-Dates: 1957 + + + +Title-Type: Reduction of Piano Concerto +Title-No: 2 +Title-For: two pianos +Title-Opus: 102a +Title-Dates: 1957 + + + +Title-Type: Symphony +Title-No: 11 +Title-Key: G minor +Title-Name: The Year 1905 +Title-Opus: 103 +Title-Dates: 1957 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 11 +Title-For: piano four hands +Title-Opus: 103a +Title-Dates: 1957 + + + +Title-Name: Cultivation: Two Russian Folk Song Arrangements +Title-For: chorus a cappella +Title-Opus: 104 +Title-Dates: 1957 + + + +Title-Count: Eleven +Title-Type: Variations +Title-Related-On: a Theme by Glinka +Title-For: piano +Title-Opus: 104a +Title-Dates: 1957 + + + +Title-Name: Moscow, Cheryomushki +Title-Type-After-Name: operetta in three acts +Title-Opus: 105 +Title-Dates: 1958 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Cheryomushki +Title-Opus: 105a +Title-Dates: 1962 + + + +Title-Type: Re-orchestration +Title-Related-How: of Mussorgsky's Opera +Title-Related-Name: Khovanshchina +Title-Opus: 106 +Title-Dates: 1959 + + + +Title-Type: Cello Concerto +Title-No: 1 +Title-Key: E flat major +Title-Opus: 107 +Title-Dates: 1959 + + + +Title-Type: Reduction of Cello Concerto +Title-No: 1 +Title-For: cello and piano +Title-Opus: 107a +Title-Dates: 1959 + + + +Title-Type: String Quartet +Title-No: 7 +Title-Key: F sharp minor +Title-Opus: 108 +Title-Dates: 1960 + + + +Title-Name: Satires (Pictures of the Past) +Title-RAW: , Five Romances on Verses by Chorny +Title-For: soprano and piano +Title-Opus: 109 +Title-Dates: 1960 + + + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Satires (Pictures of the Past) +Title-RAW: , Five Romances on Verses by Chorny for soprano and orchestra (Orchestration by Boris Tishchenko) +Title-Opus: 109a + + + +Title-Type: String Quartet +Title-No: 8 +Title-Key: C minor +Title-Opus: 110 +Title-Dates: 1960 + + + +Title-RAW: Chamber Symphony in C minor (Arr. Rudolf Barshai) +Title-Opus: 110a + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Five Days - Five Nights +Title-Opus: 111 +Title-Dates: 1960 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Five Days - Five Nights +Title-For: orchestra +Title-Opus: 111a +Title-Dates: 1961 + + + +Title-Name: Novorossiisk Chimes, the Flame of Eternal Glory +Title-For: orchestra +Title-Opus: 111b +Title-Dates: 1960 + + + +Title-Type: Symphony +Title-No: 12 +Title-Key: D minor +Title-Name: The Year 1917 +Title-Opus: 112 +Title-Dates: 1961 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 12 +Title-For: two pianos, four hands +Title-Opus: 112a +Title-Dates: 1961 + + + +Title-Type: Symphony +Title-No: 13 +Title-Key: B flat minor +Title-Name: Babi-Yar +Title-For: bass, bass chorus and orchestra +Title-Opus: 113 +Title-Dates: 1962 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 13 +Title-For: two pianos four hands +Title-Opus: 113a +Title-Dates: 1962 + + + +Title-Name: Katerina Izmailova +Title-Type-After-Name: opera in four acts +Title-Related-By: after Leskov +Title-Opus: 114 +Title-Dates: 1956-1963 + + + +Title-Type: Suite of Five Fragments +Title-Related-How: from the Opera +Title-Related-Name: Katarina Izmailova +Title-For: orchestra +Title-Opus: 114a +Title-Dates: 1963 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Katerina Izmailova +Title-Opus: 114b +Title-Dates: 1966 + + + +Title-Type: Passacaglia +Title-Related-How: from the Opera +Title-Related-Name: Katerina Izmailova +Title-For: organ +Title-Opus: 114c + + + +Title-Name: Overture on Russian and Khirghiz Folk Themes +Title-For: orchestra +Title-Opus: 115 +Title-Dates: 1963 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Hamlet +Title-Related-By: after Shakespeare +Title-For: orchestra +Title-Opus: 116 +Title-Dates: 1963-1964 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: Hamlet +Title-For: orchestra +Title-Opus: 116a +Title-Dates: 1964 + + + +Title-Type: String Quartet +Title-No: 9 +Title-Key: E flat major +Title-Opus: 117 +Title-Dates: 1964 + + + +Title-Type: String Quartet +Title-No: 10 +Title-Key: A flat major +Title-Opus: 118 +Title-Dates: 1964 + + + +Title-Name: Symphony for Strings +Title-RAW: in A-flat major (Arr. Rudolf Barshai) +Title-Opus: 118a + + + +Title-Name: The Execution of Stepan Razin +Title-Type-After-Name: cantata +Title-Related-By: after Yevtushenko +Title-For: bass, mixed chorus and orchestra +Title-Opus: 119 +Title-Dates: 1964 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: The Execution of Stepan Razin +Title-For: voices and piano +Title-Opus: 119a +Title-Dates: 1964 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: A Year Is Like a Lifetime +Title-For: orchestra +Title-Opus: 120 +Title-Dates: 1965 + + + +Title-Type: Suite +Title-Related-How: from +Title-Related-Name: A Year Is Like a Lifetime +Title-For: orchestra +Title-Opus: 120a +Title-Dates: 1965 + + + +Title-RAW: Five Romances on Texts from the Magazine +Title-Name: Krokodil +Title-For: bass and piano +Title-Opus: 121 +Title-Dates: 1965 + + + +Title-RAW: Orchestration of Five Romances on texts from the Magazine +Title-Name: Krokodil +Title-RAW: (Orcestration by Boris Tishchenko) +Title-Opus: 121a + + + +Title-Type: String Quartet +Title-No: 11 +Title-Key: F minor +Title-Opus: 122 +Title-Dates: 1966 + + + +Title-Name: Preface to the Complete Collection of My Works and Brief Reflections on this Preface +Title-For: bass and piano +Title-Opus: 123 +Title-Dates: 1966 + + + +Title-Count: Two +Title-Type: Choruses +Title-Related-By: after Davidenko +Title-For: chorus and orchestra +Title-Opus: 124 +Title-Dates: 1962 + + + +Title-Type: Transcription +Title-Related-How: of +Title-Related-Name: Songs and Dances of Death +Title-Related-By: by Mussorgsky +Title-For: voice and piano +Title-Opus: 124a +Title-Dates: 1962 + + + +Title-Name: The Lady and the Hooligan +Title-Type-After-Name: ballet in one act +Title-Opus: 124b + + + +Title-RAW: Instrumentation of Schumann's Cello Concerto +Title-Key: A minor +Title-Opus: 125 +Title-Dates: 1963 + + + +Title-Type: Cello Concerto +Title-No: 2 +Title-Key: G minor +Title-Opus: 126 +Title-Dates: 1966 + + + +Title-Type: Reduction of Cello Concerto +Title-No: 2 +Title-For: cello and piano +Title-Opus: 126a +Title-Dates: 1966 + + + +Title-Name: Seven Romances on Poems by Blok +Title-For: soprano, violin, cello and piano +Title-Opus: 127 +Title-Dates: 1967 + + + +Title-RAW: Romance "Spring, Spring" to Verses by Pushkin +Title-For: bass and piano +Title-Opus: 128 +Title-Dates: 1967 + + + +Title-RAW: Orchestration of the Romance "Spring, Spring" to Verses by Pushkin +Title-For: bass and orchestra +Title-Opus: 128a +Title-Dates: 1967 + + + +Title-Type: Violin Concerto +Title-No: 2 +Title-Key: C sharp minor +Title-Opus: 129 +Title-Dates: 1967 + + + +Title-Type: Reduction of Violin Concerto +Title-No: 2 +Title-For: violin and piano +Title-Opus: 129a +Title-Dates: 1967 + + + +Title-Name: Funeral-Triumphal Prelude +Title-For: orchestra +Title-Opus: 130 +Title-Dates: 1967 + + + +Title-Name: October +Title-Type-After-Name: symphonic poem +Title-Key: C minor +Title-For: orchestra +Title-Opus: 131 +Title-Dates: 1967 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: Sofya Perovskaya +Title-Opus: 132 +Title-Dates: 1967 + + + +Title-Type: String Quartet +Title-No: 12 +Title-Key: D flat major +Title-Opus: 133 +Title-Dates: 1968 + + + +Title-Type: Sonata +Title-For: violin and piano +Title-Opus: 134 +Title-Dates: 1968 + + + +Title-RAW: Reorchestration of Tishchenko's Cello Concerto +Title-No: 1 +Title-Opus: 134a +Title-Dates: 1969 + + + +Title-Type: Symphony +Title-No: 14 +Title-For: soprano, bass, string orchestra and percussion +Title-Opus: 135 +Title-Dates: 1969 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 14 +Title-For: voices and piano +Title-Opus: 135a +Title-Dates: 1969 + + + +Title-Name: Loyalty +Title-RAW: , eight ballads after Dolmatovsky for unaccompanied male chorus +Title-Opus: 136 +Title-Dates: 1970 + + + +Title-Type: Music +Title-Related-How: to the film +Title-Related-Name: King Lear +Title-Related-By: after Shakespeare +Title-Opus: 137 +Title-Dates: 1970 + + + +Title-Name: People's Lamentation +Title-Related-How: from +Title-Related-Name: King Lear +Title-RAW: , arranged +Title-For: voice and piano +Title-Opus: 137a +Title-Dates: 1970 + + + +Title-Type: String Quartet +Title-No: 13 +Title-Key: B flat minor +Title-Opus: 138 +Title-Dates: 1970 + + + +Title-Name: March of the Soviet Militia +Title-For: military band/wind orchestra +Title-Opus: 139 +Title-Dates: 1970 + + + +Title-Name: Six Romances on Verses by English Poets +Title-For: bass and chamber orchestra +Title-Opus: 140 +Title-Dates: 1971 + + + +Title-Type: Symphony +Title-No: 15 +Title-Key: A major +Title-Opus: 141 +Title-Dates: 1971 + + + +Title-Type: Reduction +Title-Related-How: of +Title-Related-Name: Symphony No. 15 +Title-For: two pianos +Title-Opus: 141a +Title-Dates: 1971 + + + +Title-Type: Transcription +Title-Related-How: of +Title-Related-Name: Serenada +Title-Related-By: by Braga +Title-For: soprano, mezzo-soprano, violin and piano +Title-Opus: 141b +Title-Dates: 1972 + + + +Title-Type: String Quartet +Title-No: 14 +Title-Key: F sharp major +Title-Opus: 142 +Title-Dates: 1972-1973 + + + +Title-Name: Six Poems by Marina Tsvetayeva +Title-Type-After-Name: suite +Title-For: contralto and piano +Title-Opus: 143 +Title-Dates: 1973 + + + +Title-Name: Six Poems by Marina Tsvetayeva +Title-Type-After-Name: suite +Title-For: contralto and chamber orchestra +Title-Opus: 143a +Title-Dates: 1974 + + + +Title-Type: String Quartet +Title-No: 15 +Title-Key: E flat minor +Title-Opus: 144 +Title-Dates: 1974 + + + +Title-Name: Epilogue +Title-Type-After-Name: Transcription of String Quartet +Title-No: 15 +Title-For: string orchestra +Title-Opus: 144a +Title-Dates: 1994 + + + +Title-RAW: Suite on Verses by Buonarrotti +Title-For: bass and piano +Title-Opus: 145 +Title-Dates: 1974 + + + +Title-RAW: Suite on Verses by Buonarrotti +Title-For: bass and orchestra +Title-Opus: 145a +Title-Dates: 1975 + + + +Title-Name: Four Verses of Captain Lebyadkin to Texts by Dostoyevsky +Title-For: bass and piano +Title-Opus: 146 +Title-Dates: 1975 + + + +## SubNumbers for Opus 146 from: http://home.wanadoo.nl/ovar/shosopus/shoslast.htm + +Title-Type: Orchestration +Title-Related-How: of +Title-Related-Name: Four Verses by Captain Lebyadkin +Title-RAW: (Orchestration by Boris Tishchenko) +Title-Opus: 146a + + + +Title-RAW: Orchestration of Beethoven's Arrangement (Opus 75 No. 3) +Title-Related-How: of Mephistopheles's +Title-Related-Name: Song of the Flea +Title-Opus: 146b +Title-Dates: 1975 + + + +Title-Name: The Dreamers +Title-Type-After-Name: ballet in four acts +Title-Opus: 146c +Title-Dates: 1975 + + + +Title-Type: Sonata +Title-For: viola and piano +Title-Opus: 147 +Title-Dates: 1975 + + + +Title-RAW: Transcription of Opus 147 +Title-For: cello and piano +Title-Opus: 147a +Title-Dates: 1975 + + + +Title-RAW: Transcription of Opus 147 +Title-For: viola, strings and celesta +Title-Opus: 147b +Title-Dates: 1990 + + + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/G_Gershwin.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/G_Gershwin.comp new file mode 100644 index 000000000..9b5ed5d81 --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/G_Gershwin.comp @@ -0,0 +1,906 @@ +# format = mail-header + + +# no_opus_no + + + +## Note: All orchestral/operatic pieces are orchestrated by Gershwin unless otherwise specified. +## * Lullaby (1919), a meditative piece for string quartet. Originally, a class assignment from his music theory teacher. +## * Blue Monday, a one-act opera featured in George White's Scandals of 1922, orchestrated by Will Vodery. +## + A Suite from Blue Monday for two pianos was later arranged and has been recorded. +## + Reorchestrated by Ferde Grofe and retitled 135th Street in 1925. +## * Rhapsody in Blue, (1924), his most famous work, a symphonic jazz composition for Paul Whiteman's jazz band & piano , better known in the form orchestrated for full +## symphonic orchestra by Ferde Grofe. Featured in numerous films and commercials. + +Title-RAW: Short Story, +Title-For: violin and piano +Title-Dates: 1925 + +##, an arrangement of two other short pieces originally intended to be included with the Three Preludes. +## * Concerto in F, (1925), three movements, for piano and orchestra +## * Three Preludes, (1926), for piano +## * An American In Paris (1928), a symphonic poem with elements of jazz and realistic Parisian sound effects +## * Second Rhapsody for Piano and Orchestra (1931), for Piano and Orchestra, based on the score for a musical sequence from Delicious. Working title for the work was Rhapsody +## in Rivets. +## + The form most commonly heard today is a re-orchestrated version by Robert McBride; most of Gershwin's orchestrations have been simplified. Also, eight measures not by the composer +## were added to the recapitulation. Michael Tilson Thomas has been a promulgator of Gershwin's original version. +## * Cuban Overture (1932), originally titled Rumba, a tone poem featuring elements of native Cuban dance and folk music; score specifies usage of native Cuban instruments + +Title-RAW: Piano Transcriptions of Eight Songs +Title-Dates: 1932 + +## * I Got Rhythm Variations (1934), a set of interesting variations on his famous song, for piano and orchestra +## + Includes a waltz, an atonal fugue, and experimentation with Asian and jazz influences +## * Porgy And Bess, a folk opera (1935) (from the book by DuBose Heyward) about African-American life, now considered a definitive work of the American theater. +## + Contains the famous aria "Summertime", in addition to hits like "I Got Plenty of Nothin'" and "It Ain't Necessarily So". +## + Porgy and Bess has also been heard in the concert hall, mostly in two orchestral suites, one by Gershwin himself entitled Catfish Row; another suite by Robert Russell +## Bennett, Porgy and Bess: A Symphonic Picture is also relatively popular. + +Title-RAW: Walking the Dog +Title-Dates: 1937 + +##, a humorous piece for orchestra featuring the clarinet. Originally a musical sequence entitled Promenade from the movie Shall We Dance for piano +## and chamber orchestra. +## + Many other incidental sequences from Shall We Dance were written and (for the most part) orchestrated by Gershwin, among them: Waltz of the Red Balloons and a final +## extended 8-minute orchestral passage based on the title song with an intruiging coda hinting at Gershwin forging a new musical path. It is unknown why any of these +## compositions have not seen the light of day in the concert hall. +## + Most of the musicals Gershwin wrote are also known for their instrumental music, among them the March from Strike Up The Band and overtures to many of his later +## shows. + +Title-RAW: Impromptu in Two Keys +Title-Dates: publ. posth. in 1973 + +##, for piano + +Title-Count: Two +Title-Type: Waltzes +Title-Key: C major +Title-Dates: publ. posth. in 1975 + +##, for piano +## + Originally a two-piano interlude in Pardon My English on Broadway. + + + +## Musical theater credits + + + +## Note: All works are musicals produced on Broadway unless specified otherwise. + + + +Title-RAW: Half Past Eight (lyrics by Ira Gershwin and Edward B. Perkins). Premiered in Syracuse. +Title-Dates: 1919 + +Title-RAW: La La Lucille +Title-Lyrics-By: Arthur Jackson, B. G. DeSylva and Irving Caesar +Title-Dates: 1919 + +Title-RAW: Morris Gest +Title-Name: Midnight Whirl +Title-Lyrics-By: B. G. DeSylva and John Henry Mears +Title-Dates: 1919 + +Title-RAW: Limehouse Nights +Title-Lyrics-By: B. G. DeSylva and John Henry Mears +Title-Dates: 1919 + +Title-RAW: Poppyland +Title-Lyrics-By: B. G. DeSylva and John Henry Mears +Title-Dates: 1920 + +Title-RAW: George White's Scandals of 1920 +Title-Lyrics-By: Arthur Jackson +Title-Dates: 1920 + +Title-RAW: A Dangerous Maid (lyrics by Ira Gershwin). Premiered in Atlantic City. +Title-Dates: 1921 + +Title-RAW: The Broadway Whirl (co-composed with Harry Tierney, lyrics by Buddy DeSylva, Joseph McCarthy, Richard Carle and John Henry Mears +Title-Dates: 1921 + +Title-RAW: George White's Scandals of 1921 +Title-Lyrics-By: Arthur Jackson +Title-Dates: 1921 + +Title-RAW: George White's Scandals of 1922 +Title-Lyrics-By: E. Ray Goetz, Ira Gershwin and B. G. DeSylva +Title-Dates: 1922 + +## + The premiere performance featured the one-act opera Blue Monday with libretto and lyrics by B. G. DeSylva, set in Harlem in a jazz idiom. However, after only one +## performance, the opera was withdrawn from the show. Gershwin also wrote seven other songs for the show. + +Title-RAW: Our Nell (co-composed with William Daly, lyrics co-written by Gershwin and Daly) +Title-Dates: 1922 + +Title-RAW: By and By +Title-Lyrics-By: Brian Hooker +Title-Dates: 1922 + +Title-RAW: Innocent Ingenue Baby (co-composed with William Daly, lyrics by Brian Hooker) +Title-Dates: 1923 + +Title-RAW: Walking Home with Angeline +Title-Lyrics-By: Brian Hooker +Title-Dates: 1923 + +Title-RAW: The Rainbow (lyrics by Clifford Grey and Brian Hooker). Premiered in London. +Title-Dates: 1923 + +Title-RAW: George White's Scandals of 1923 +Title-Lyrics-By: E. Ray Goetz, B. G. DeSylva and Ballard MacDonald +Title-Dates: 1923 + +Title-RAW: Sweet Little Devil +Title-Lyrics-By: B. G. DeSylva +Title-Dates: 1924 + +Title-RAW: George White's Scandals of 1924 +Title-Lyrics-By: B. G. DeSylva and Ballard MacDonald +Title-Dates: 1924 + +Title-RAW: Primrose (lyrics by Desmond Carter and Ira Gershwin). Premiered in London. +Title-Dates: 1924 + +Title-RAW: Lady, Be Good! +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1924 + +Title-RAW: Tell Me More! +Title-Lyrics-By: Ira Gershwin and B. G. DeSylva +Title-Dates: 1925 + +Title-RAW: Tip-Toes +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1925 + +Title-RAW: Song of the Flame (operetta, lyrics by Otto Harbach and Oscar Hammerstein II, and musical collaboration by Herbert Stothart) +Title-Dates: 1925 + +Title-RAW: Oh, Kay! +Title-Lyrics-By: Ira Gershwin and Howard Dietz +Title-Dates: 1926 + +## + Includes the famous song, "Someone to Watch Over Me" +## + Revived in 1928 and 1990 (the latter with an all-Black cast) + + + +Title-RAW: Strike Up The Band +Title-Lyrics-By: Ira Gershwin +Title-Dates: premiered in Philadelphia 1927, revised Broadway in 1930, revised 1936 for U.C.L.A + +# prev_aka Strike Up The Band + + + +## + Revised and produced on Broadway in 1930 + +Title-RAW: Funny Face +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1927 + +Title-RAW: Rosalie +Title-Lyrics-By: Ira Gershwin and P. G. Wodehouse, co-composed with Sigmund Romberg +Title-Dates: 1928 + +Title-RAW: Treasure Girl +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1928 + +Title-RAW: Show Girl +Title-Lyrics-By: Ira Gershwin and Gus Kahn +Title-Dates: 1929 + +Title-RAW: Girl Crazy +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1930 + +Title-RAW: Of Thee I Sing +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1931 + +## + Awarded the Pulitzer Prize for Drama for 1932 and was the first musical to win that award, although only Ira Gershwin and the bookwriters were awarded the Prize and +## not George Gershwin +## + Revived in 1933 and 1952 + +Title-RAW: Pardon My English +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1933 + +Title-RAW: Let 'Em Eat Cake +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1933 + +## Let 'Em Eat Cake (lyrics by Ira Gershwin), sequel to Of Thee I Sing (1933) + +Title-RAW: Porgy and Bess +Title-Lyrics-By: Ira Gershwin and DuBose Heyward +Title-Dates: 1935 + +## + Revived on Broadway in 1942, 1943, 1953, 1976 (Houston Grand Opera winner of the Tony Award for Most Innovative Revival of a Musical), and 1983 + + + +## Works featuring original Gershwin songs for shows by other composers + + + +Title-RAW: The Passing Show of 1916 - "Making of a Girl" co-composed with Sigmund Romberg, lyrics by Harold Atteridge +Title-Dates: 1916 + +Title-RAW: Hitchy-Koo of 1918 - "You-Oo just You", lyrics by Irving Caesar +Title-Dates: 1918 + +Title-RAW: Ladies First - "(The Real) American Folk Song (is a Rag)", lyrics by Ira Gershwin and "Some Wonderful Sort of Someone", lyrics by Schuyler Greene +Title-Dates: 1918 + +## * 1919 - Good Morning, Judge - "I was so young (you were so beautiful)", lyrics by Irvine Caesar and Alfred Bryan and "here's more to the kiss than the x-x-x", lyrics by +## Irving Caesar + +Title-RAW: The Lady in Red - "Some Wonderful Sort of Someone", lyrics by Schyler Greene and "Something about Love", lyrics by L. Paley +Title-Dates: 1919 + +Title-RAW: The Capitol Revue - "Come to the Moon", lyrics by L. Paley and Ned Wayburn, "Swanee", lyrics by Irvine Caesar +Title-Dates: 1919 + +Title-RAW: Dear Mabel - "We're pals", lyrics by Irving Caesar, first performed in Baltimore +Title-Dates: 1920 + +Title-RAW: Ed Wynn's Carnival - "Oo, how I love you to be loved by you", lyrics by L. Paley +Title-Dates: 1920 + +Title-RAW: The Sweetheart Shop - "Waiting for the Sun to Come Out", lyrics by Ira Gershwin +Title-Dates: 1920 + +Title-RAW: Sinbad - "Swanee" (as performed by Al Jolson) +Title-Dates: 1920 + +Title-RAW: Broadway Brevities of 1920 - "Lu Lu" and "Snowflakes", lyrics by Arthur Jackson and "Spanish love", lyrics by Irving Caesar +Title-Dates: 1920 + +Title-RAW: Piccadilly to Broadway, songs unpublished +Title-Dates: 1920 + +Title-RAW: Blue Eyes, songs unpublished +Title-Dates: 1921 + +Title-RAW: Selwyn's Snapshots of 1921, songs unpublished +Title-Dates: 1921 + +Title-RAW: The Perfect Fool - "My Log-Cabin Home", lyrics by Irving Caesar and Buddy De Sylva, "No One Else but that Girl of Mine", lyrics by Irving Caesar +Title-Dates: 1921 + +Title-RAW: The French Doll - "Do it again!", lyrics by Buddy De Sylva +Title-Dates: 1922 + +Title-RAW: For Goodness Sake - "Someone" and "Tra-la-la", lyrics by Ira Gershwin +Title-Dates: 1922 + +Title-RAW: The Dancing Girl - "That American Boy of Mine", lyrics by Irving Caesar +Title-Dates: 1922 + +Title-RAW: Spice of 1922 - "The Yankee Doodle Blues", lyrics by Irving Caesar and Buddy De Sylva +Title-Dates: 1922 + +Title-RAW: Little Miss Bluebeard (play) - "I won't say I will but I won't say I won't", lyrics by Ira Gershwin and Buddy De Sylva +Title-Dates: 1923 + +Title-RAW: Nifties of 1923 - "At Half Past Seven", lyrics by Buddy De Sylva, and "Nashville Nightingale", lyrics by Irving Caesar +Title-Dates: 1923 + +Title-RAW: Americana of 1926 - +Title-Name: That Lost Barber Shop Chord +Title-Dates: 1926 + +Title-RAW: 9:15 Revue +Title-Dates: 1930 + +Title-RAW: The Show is On - +Title-Name: By Strauss +Title-Dates: 1936 + +## + Revived in 1937 + + + +## Works interpolating Gershwin songs posthumously: + + + +Title-RAW: At Home With Ethel Waters - +Title-Name: Lady Be Good +Title-Dates: 1953 + +Title-RAW: Mr. Wonderful +Title-Dates: 1956 + +### "I Got Rhythm" a hit single for pop vocal group The Happenings (1967) +### My One And Only - an adaptation of the music from Funny Face (1983) +### Uptown...It's Hot! - "Lady Be Good" (1986) +### Crazy For You - musical adapting George and Ira Gershwin Tin Pan Alley and Broadway songs (1992) +## + Awarded the Tony Award for Best Musical +### The Gershwins' Fascinating Rhythm - revue with songs by George and Ira Gershwin (1999) +## * 2001 - George Gershwin Alone - one-man play by Hershey Felder, who portrayed Gershwin, incorporating "Swanee" from Sinbad (lyrics by Irving Caesar), "Embraceable You" from +## Girl Crazy (lyrics by Ira Gershwin), "Someone to Watch Over Me" from Oh, Kay! (lyrics by Ira Gershwin), "Bess, You is My Woman Now" from Porgy and Bess +## (lyrics by DuBose Heyward and Ira Gershwin), An American in Paris and Rhapody in Blue. +### Elaine Stritch at Liberty - But Not For Me (2002) +### Back From Broadway - one-time concert featuring songs by George Gershwin (2002) + + + +## Musical films + + + +Title-RAW: The Sunshine Trail - theme song of same title (lyrics by Ira Gershwin), as well as accompaniment music for silent film +Title-Dates: 1923 + +Title-RAW: Delicious +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1931 + +Title-RAW: Shall We Dance? +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1937 + +Title-RAW: A Damsel in Distress +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1937 + +Title-RAW: Goldwyn Follies +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1938 + +## + Gershwin died during the filming. Vernon Duke completed and adapted Gerhwin's songs, and composed some additional ones. + +Title-RAW: The Shocking Miss Pilgrim (Kay Swift adapted a number of unpublished Gershwin melodies and Ira Gershwin wrote the lyrics.) +Title-Dates: 1947 + +Title-RAW: Kiss Me, Stupid (adaptations of unpublished Gershwin songs with lyrics by Ira Gershwin.) +Title-Dates: 1964 + + + +## Miscellaneous Songs + + + +Title-RAW: When you want 'em, you can't get 'em, when you've got 'em, you don't want 'em +Title-Lyrics-By: M. Roth +Title-Dates: 1916 + +Title-RAW: The Love of a Wife +Title-Lyrics-By: Arthur Jackson and B. G. De Sylva +Title-Dates: 1919 + +Title-RAW: Yan-Kee +Title-Lyrics-By: Irving Caesar +Title-Dates: 1920 + +Title-RAW: Dixie Rose +Title-Lyrics-By: Irving Caesar and B. G. De Sylva +Title-Dates: 1921 + +Title-RAW: In the Heart of a Geisha +Title-Lyrics-By: Fred Fisher +Title-Dates: 1921 + +Title-RAW: Swanee Rose +Title-Lyrics-By: Irving Caesar and B. G. De Sylva +Title-Dates: 1921 + +Title-RAW: Tomale (I'm hot for you) +Title-Lyrics-By: B. G. De Sylva +Title-Dates: 1921 + +Title-RAW: Harlem River Chanty and It's a great little world! +Title-Lyrics-By: Ira Gershwin, originally composed for Tip-Toes on Broadway but not used +Title-Dates: 1925 + +Title-RAW: Murderous Monty (and Light-Fingered Jane) +Title-Lyrics-By: Desmond Carter, composed for London production of Tell Me More. +Title-Dates: 1925 + +Title-RAW: I'd rather charleston +Title-Lyrics-By: Desmond Carter, composed for London production of Lady Be Good. +Title-Dates: 1926 + +Title-RAW: Beautiful gypsy and Rosalie (originally composed for Rosalie on Broadway, but not used) +Title-Dates: 1928 + +Title-RAW: Feeling Sentimental (originally composed for Show Girl on Broadway, but not used) +Title-Dates: 1929 + +Title-RAW: In the Mandarin's Orchid Garden +Title-Dates: 1929 + +Title-RAW: Mischa, Yascha, Toscha, Sascha (originally composed for the musical film Delicious, but not used. +Title-Dates: 1931 + +## + This is Gershwin's only finished work based on a Jewish theme, and the title is a reference to the first names of four Jewish-Russian violinists, Mischa Elman, +## Jascha Heifetz, Toscha Seidel and Sascha Jacobsen. + +Title-RAW: You've got what gets me (for 1st film version of Girl Crazy) +Title-Dates: 1932 + +Title-RAW: Till Then +Title-Dates: 1933 + +Title-RAW: King of Swing +Title-Lyrics-By: Al Stillman +Title-Dates: 1936 + +Title-RAW: Strike up the band for U.C.L.A (to the same music as the song Strike Up The Band) +Title-Dates: 1936 + + + +Title-RAW: Hi-Ho! +Title-Lyrics-By: Ira Gershwin +Title-Dates: 1937 + +## originally composed for Shall We Dance, but not used + + + +Title-RAW: Just Another Rhumba +Title-Lyrics-By: Ira Gershwin, originally composed for The Goldwyn Follies, but not used +Title-Dates: 1938 + +Title-RAW: Dawn of a New Day +Title-Dates: 1938 + + + +## Commercial Works for Piano + + + +Title-RAW: Rialto Ripples - rag +Title-Dates: 1918 + +## * early 1920s - Three-Quarter Blues (Irish Waltz) + +Title-RAW: Swiss Miss (arrangement of a song from Lady Be Good) +Title-Dates: 1926 + +Title-RAW: Merry Andrew (arrangement of a dance piece from Rosalie) +Title-Dates: 1928 + +Title-RAW: George Gershwin's Song-Book (arrangements of refrains from Gershwin songs) +Title-Dates: 1932 + + + +### ==### PIANO // + +Title: 3 Preludes for Piano (1926) (transcr. for vn. and pf. by Heifetz) + +Title: Three Preludes for Piano (1926) (transcr. for vn. and pf. by Heifetz) + +Title-Count: 3 +Title-Type: Preludes +Title-For: Piano +Title-Dates: 1926 + +Title-Count: Three +Title-Type: Preludes +Title-For: Piano +Title-Dates: 1926 + +Title: Prelude No. 1 for Piano (1926) - Allegro ben ritmato e deciso + +Title: Prelude No. 2 for Piano (1926) - Andante con moto e poco rubato + +Title: Prelude No. 3 for Piano (1926) - Allegro ben ritmato e deciso + +### ==### MUSICALS // +### The Passing Show of 1916 +### ###MUSICALS // +### La La Lucille (1919) +### ###MUSICALS // +### George White's Scandals (1920--1924) +### ###MUSICALS // +### A Dangerous Maid (1921) +### ###MUSICALS // +### Sweet Little Devil (1924) +### ###MUSICALS // +### Primrose (1924) +### ###MUSICALS // +### Lady, Be Good! (1924) +### ###MUSICALS // +### Song of the Flame (1925) +### ###MUSICALS // +### Tell Me More (1925) +### ###MUSICALS // +### Tip Toes (1925) +### ###MUSICALS // +### Oh, Kay! (1926, lyrics by P. G. Wodehouse) +### ###MUSICALS // +### Strike up the Band (1927, 2nd vers. 1930) +### ###MUSICALS // +### Funny Face (1927) +### ###MUSICALS // +### Rosalie (1928) +### ###MUSICALS // +### Treasure Girl (1928) +### ###MUSICALS // +### Show Girl (1929) +### ###MUSICALS // +### Girl Crazy (1930) +### ###MUSICALS // +### Of Thee I Sing (1931, lyrics by George F. Kaufman) +### ###MUSICALS // +### Pardon my English (1933) +### ###MUSICALS // +### Let 'em eat Cake (1933) +### ==### SONGS // +### ## Among the best of hundreds of songs are Swanee; The Man I Love; Embraceable You; I Got Rhythm; Fascinating Rhythm; 'S Wonderful; Lady Be Good; and Love Walked In. The popular Summertime is from Porgy and Bess +### ###FILMS // +### Delicious (1931) +### ###FILMS // +### Shall We Dance?; A Damsel in Distress (1937) +### ###FILMS // +### The Goldwyn Follies (1938) +### ###FILMS // +### The Shocking Miss Pilgrim (1946) +### ###FILMS // +### Kiss Me, Stupid (1964) +### ###OPERAS // + +Title-RAW: Blue Monday +Title-Dates: 1-act; item in George White's Scandals 1922 but withdrawn after 1 perf.; retitled 135th Street and revived Miami 1970 + +### ###OPERAS // +### Porgy and Bess (1934--1935) +### ###ORCH. // + +Title-RAW: Rhapsody in Blue (pf. and orch.) +Title-Dates: 1924 + +### ###ORCH. // + +Title-Type: Piano concerto +Title-Key: F major +Title-Dates: 1925 + +### ###ORCH. // + +Title-RAW: An American in Paris +Title-Dates: 1928 + +### ###ORCH. // + +Title-RAW: Second Rhapsody for Piano and orch. (working title "Rhapsody in Rivets") +Title-Dates: 1931 + +### ###ORCH. // + +Title-RAW: Cuban Overture +Title-Dates: 1932 + +### ###ORCH. // + + + +Title-RAW: "I Got Rhythm" Variations for Piano and orch. +Title-Dates: 1934 + +# prev_aka Variations on "I Got Rhythm" + + + +### +### +### ## http://www.gershwinfan.com/works.html +### +### Since I Found You (1913) +### When You Want 'Em, You Can't Get 'Em; When You've Got 'Em, You Don't Want 'Em (1916) +### ## The Passing Show of 1916 (1916) +### Rialto Ripples (1917) +### Beautiful Bird (1917) +### You Are Not the Girl (1917) +### Hitchy Koo of 1918 (1918) +### The Real American Folk Song (Is a Rag) (1918) +### Kitchenette (1918) +### If You Only Knew (1918) +### There's Magic in the Air (1918) +### When There's a Chance to Dance (1918) +### ## La, La, Lucille (1919) +### Morris Gest Midnight Whirl (1919) +### Lullaby (1919) +### Good Morning, Judge (1919) +### The Lady in Red (1919) +### Capitol Revue (1919) +### George White's Scandals of 1920 (1920) +### Piccadilly to Broadway (1920) +### For No Reason at All (1920) +### Mischa, Jascha, Toscha, Sascha (1920) +### Waiting for the Sun to Come Out (1920) +### Back Home (1920) +### I Want to Be Wanted by You (1920) +### Ed Wynn's Carnival (1920) +### Sinbad (1920) +### Broadway Brevities of 1920 (1920) +### George White's Scandals of 1921 (1921) +### The Perfect Fool (1921) +### Blue Eyes (1921) +### Selwyn's Snapshots of 1921 (1921) +### George White's Scandals of 1922 (1922) +### ## Blue Monday (one-act opera) (1922) +### Molly on the Shore (1922) +### For Goodness Sake (1922) +### A New Step Ev'ry Day a/k/a Stairway to Paradise (1922) +### Our Nell (1922) +### The French Doll (1922) +### ## A Dangerous Maid (1921) +### Phoebe (1921) +### Spice of 1922 (1922) +### The Rainbow (1923) +### George White's Scandals of 1923 (1923) +### The Dancing Girl (1923) +### Nifties of 1923 (1923) +### I Won't Say I Will but I Won't Say I Won't (1923) +### The Sunshine Trail (1923) +### ## Rhapsody in Blue (1924) +### George White's Scandals of 1924 (1924) +### ## Lady, Be Good! (1924) +### ## Sweet Little Devil (1924) +### ## Primrose (1924) +### ## Concerto in F (1925) +### ## Song of the Flame (1925) +### Short Story (1925) +### ## Tell Me More (1925) +### ## Tip-Toes (1925) +### Preludes for Piano (1926) +### Americana (1926) +### ## Oh, Kay! (1926) +### ## Strike Up the Band (1927) +### ## Funny Face (1927) +### ## Treasure Girl (1928) +### ## An American in Paris (1928) +### ## Rosalie (1928) +### ## Show Girl (1929) +### Impromptu in Two Keys (1929) +### Three-Quarter Blues (1929) +### East is West (1929) +### 9:15 Review (1930) +### ## Girl Crazy (1930) +### Strike Up the Band (revision) (1930) +### ## Of Thee I Sing (1931) +### ## Delicious (1931) +### George Gershwin's Song-Book (1932) +### ## Second Rhapsody (1932) +### ## Cuban Overture (1932) +### Girl Crazy (1932) +### ## Pardon My English (1933) +### ## Let 'Em Eat Cake (1933) +### ## Variations on I Got Rhythm (1934) +### ## Porgy and Bess (1935) +### The Show is On (1936) +### Suite from Porgy and Bess (1936) +### ## Shall We Dance? (1937) +### A Damsel in Distress (1937) +### ## The Goldwyn Follies (1937) + + + +Title: Summertime (Act I Scene 1) + +Title: A Woman is a Sometime Thing (Act I Scene 1) + + + +Title: My Man's Gone Now (Act I Scene 2) + +# prev_aka MY MANS GONE NOW + + + +Title: It Take a Long Pull to Get There (Act II Scene 1) + +Title: I Got Plenty o' Nuttin' (Act II Scene 1) + + + +Title: Buzzard Keep on Flyin' (Act II Scene 1) + +# prev_aka THE BUZZARD SONG + + + +Title: Bess, You Is My Woman Now (Act II Scene 1) + +Title: Oh, I Can't Sit Down (Act II Scene 1) + + + +Title: It Ain't Necessarily So (Act II Scene 2) + +## Misprints: + +# prev_aka It Aint Necessarily So (Act II Scene 2) + +# prev_aka It Aint Necessarily So + + + +Title: What you want wid Bess (Act II Scene 2) + +Title: Oh, Doctor Jesus (Act II Scene 3) + +Title: A Red-Haired Woman (Act II Scene 4) + +Title: There's a Boat Dat's Leavin' Soon for New York (Act III Scene 2) + + + +Title: Bess, O Where's My Bess? (Act III Scene 3) + +# prev_aka Where is my Bess + + + +Title: I'm on my way (Act III Scene 3) + + + +### From Ella's record + + + +Title: The Half Of It Dearie Blues + +Title: 'S Wonderful + +Title: Aren't You Kind Of Glad We Did? + +Title: (I've Got) Beginner's Luck + +Title: Bidin' My Time + +Title: Boy Wanted + +Title: Boy! What Love Has Done To Me! + +Title: But Not For Me + +Title: By Strauss + +Title: Cheerful Little Earful + +Title: Clap Yo' Hands + +Title: Embraceable You + +Title: Fascinating Rhythm + +Title: Fascinatin' Rhythm + +Title: Fidgety Feet + +Title: A Foggy Day + +Title: For You, For Me, For Evermore + +## Funny Face + +Title: He Loves And She Loves + +Title: How Long Has This Been Going On? + +Title: I Can't Be Bothered Now + +Title: I Got Rhythm + +Title: I Was Doing All Right + +Title: I've Got A Crush On You + +Title: Isn't It A Pity? + +Title: Just Another Rhumba + +Title: Let's Call The Whole Thing Off + +Title: Let's Kiss And Make Up + +Title: Looking For A Boy + +Title: Lorelei + +### Lorelei - (alternate take) + +Title: Love Is Here To Stay + +### Love Is Here To Stay - (alternate take) + +Title: Love Is Sweeping The Country + +Title: Love Walked In + +Title: The Man I Love + +Title: March Of The Swiss Soldiers + +Title: My Cousin In Milwaukee + +Title: My One And Only + +Title: Nice Work If You Can Get It + +### Of Thee I Sing + +Title: Oh, Lady, Be Good! + +### Oh, Lady, Be Good! - (alternate take) + +Title: Oh, So Nice! + +Title: Prelude I + +Title: Prelude II + +Title: Prelude III + +Title: Promenade (Walking The Dog) + +Title: The Real American Folk Song (Is A Rag) + +Title: Sam And Delilah + +### Shall We Dance? + +Title: Slap That Bass + +Title: Somebody From Somewhere + +Title: Somebody Loves Me + +Title: Someone To Watch Over Me + +Title: Soon + +Title: Stiff Upper Lip + +### Strike Up The Band + +Title: That Certain Feeling + +Title: They All Laughed + +Title: They Can't Take That Away From Me + +Title: Things Are Looking Up + +Title: Treat Me Rough + +Title: Who Cares? + +### You've Got What Gets Me + + + +### From Gershwin plays Gershwin + +Title: Hang on to Me + +Title: Sweet and low down + +Title: Then do we dance? + +Title: Maybe + +Title: Do-do-do + + + +## From Porgy and Bess + +Title: I wants to stay here + +Title: Medley: Here come de honey man crab man oh + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/J_Brahms.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/J_Brahms.comp new file mode 100644 index 000000000..f4bd99f7a --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/J_Brahms.comp @@ -0,0 +1,868 @@ +# format = mail-header + + + +## This is a very primitive list, not taking into account sub-opuses + + +Title-Type: Piano Sonata +Title-No: 1 +Title-Key: C major +Title-Opus: 1 +Title-Dates: 1852 + + + +Title-Type: Piano Sonata +Title-No: 2 +Title-Key: F sharp minor +Title-Opus: 2 +Title-Dates: 1852 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 3 +Title-Dates: 1853 + + + +Title-Type: Scherzo +Title-Key: E flat minor +Title-For: piano +Title-Opus: 4 +Title-Dates: 1851 + + + +Title-Type: Piano Sonata +Title-No: 3 +Title-Key: F minor +Title-Opus: 5 +Title-Dates: 1853 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 6 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 7 + + + +Title-Type: Piano Trio +Title-No: 1 +Title-Key: B major +Title-Opus: 8 +Title-Dates: 1854 + + + +Title-RAW: Variations on a theme by Robert Schumann +Title-Key: F sharp minor +Title-For: piano +Title-Opus: 9 +Title-Dates: 1854 + + + +Title-Count: Four +Title-Type: Ballades +Title-For: piano +Title-Opus: 10 +Title-Dates: 1854 + + + +Title-Type: Serenade +Title-No: 1 +Title-Key: D major +Title-For: orchestra +Title-Opus: 11 +Title-Dates: 1857 + + + +Title-Name: Ave Maria +Title-Opus: 12 + + + +Title-Name: Begräbnisgesang +Title-Opus: 13 + + + +Title-Count: Eight +Title-Type: Songs and Romances +Title-Opus: 14 + + + +Title-Type: Piano Concerto +Title-No: 1 +Title-Key: D minor +Title-Opus: 15 +Title-Dates: 1859 + + + +Title-Type: Serenade +Title-No: 2 +Title-Key: A major +Title-For: orchestra +Title-Opus: 16 +Title-Dates: 1859 + + + +Title-Count: Four +Title-Type: Songs +Title-For: female voices, two horns and harp +Title-Opus: 17 + + + +Title-Type: String Sextet +Title-No: 1 +Title-Key: B flat major +Title-Opus: 18 +Title-Dates: 1860 + + + +Title-Count: Five +Title-Type: Poems +Title-Opus: 19 + + + +Title-Count: Three +Title-Type: Duets +Title-Opus: 20 + + + +Title-RAW: Two Sets of Variations +Title-For: piano +Title-Opus: 21 + + + +Title-Name: Marienlieder +Title-Opus: 22 + + + +Title-RAW: Variations on a Theme by Robert Schumann +Title-For: piano, four hands +Title-Opus: 23 +Title-Dates: 1861 + + + +Title-RAW: Variations and Fugue on a Theme by Handel +Title-For: piano +Title-Opus: 24 +Title-Dates: 1861 + + + +Title-Type: Piano Quartet +Title-No: 1 +Title-Key: G minor +Title-Opus: 25 +Title-Dates: 1861 + + + +Title-Type: Piano Quartet +Title-No: 2 +Title-Key: A major +Title-Opus: 26 +Title-Dates: 1861 + + + +Title-RAW: Psalm 13 +Title-Opus: 27 + + + +Title-Count: Four +Title-Type: Duets +Title-Opus: 28 + + + +Title-Count: Two +Title-Type: Motets +Title-Opus: 29 +Title-Dates: 1860, published 1864 + + + +Title-RAW: Geistliches Lied +Title-Opus: 30 + + + +Title-Count: Three +Title-Type: Vocal Quartets +Title-Opus: 31 + + + +Title-Count: Nine +Title-Type: Songs +Title-Opus: 32 + + + +Title-Count: Fifteen +Title-Type: Romances +Title-Related-How: from Tieck's +Title-Related-Name: Liebesgeschichte der schönen Magelone +Title-Opus: 33 + + + +Title-Type: Piano Quintet +Title-Key: F minor +Title-Opus: 34 +Title-Dates: 1864 + + + +Title-Type: Sonata +Title-For: 2 Pianos +Title-Key: F minor +Title-Opus: 34b + + + +Title-RAW: Variations on a Theme by Paganini +Title-For: Piano +Title-Opus: 35 +Title-Dates: 1862-1863 + + + +Title-Type: String Sextet +Title-No: 2 +Title-Opus: 36 + + + +Title-Count: Three +Title-Type: Sacred Choruses +Title-Opus: 37 + + + +Title-Type: Cello Sonata +Title-No: 1 +Title-Key: E minor +Title-Opus: 38 +Title-Dates: 1862-65 + + + +Title-Count: Sixteen +Title-Type: Waltzes +Title-For: piano, four hands +Title-Opus: 39 +Title-Dates: 1865 + + + +Title-Type: Trio +Title-For: Horn, Violin and Piano +Title-Key: E flat major +Title-Opus: 40 +Title-Dates: 1865 + + + +Title-Count: Five +Title-Type: Songs +Title-For: male voices +Title-Opus: 41 + + + +Title-Count: Three +Title-Type: secular songs +Title-For: choir +Title-Opus: 42 + + + +Title-Count: Four +Title-Type: Songs +Title-Opus: 43 + + + +Title-Count: Twelve +Title-Type: Songs and Romances +Title-Opus: 44 + + + +Title-Name: Ein deutsches Requiem +Title-Opus: 45 +Title-Dates: 1868 + + + +Title-Count: Four +Title-Type: Songs +Title-Opus: 46 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 47 + + + +Title-Count: Seven +Title-Type: Songs +Title-Opus: 48 + + + +Title-RAW: Five Songs -- (#4, "Wiegenlied", is also known as "Brahms' Lullaby") +Title-Opus: 49 + + + +Title-Name: Rinaldo +Title-Opus: 50 + + + +Title-Count: Two +Title-Type: String Quartets +Title-Opus: 51 + + + +Title-Count: Eighteen +Title-Name: Liebeslieder-Waltzer +Title-For: piano, four hands and vocal quartet +Title-Name: ad libitum +Title-Opus: 52 +Title-Dates: 1874 + + + +Title-RAW: Alto Rhapsody +Title-Opus: 53 + + + +Title-Name: Schicksalslied +Title-Opus: 54 + + + +Title-Name: Triumphlied +Title-Opus: 55 + + + +Title-RAW: Variations on a Theme by Joseph Haydn +Title-Opus: 56 +Title-Dates: 1873 + + + +Title-Count: Eight +Title-Type: Songs +Title-Opus: 57 + + + +Title-Count: Eight +Title-Type: Songs +Title-Opus: 58 + + + +Title-Count: Eight +Title-Type: Songs +Title-Opus: 59 + + + +Title-Type: Piano Quartet +Title-No: 3 +Title-Key: C minor +Title-Opus: 60 + + + +Title-Count: Four +Title-Type: Duets +Title-Opus: 61 + + + +Title-Count: Seven +Title-Type: secular songs +Title-For: choir +Title-Opus: 62 + + + +Title-Count: Nine +Title-Type: Songs +Title-Opus: 63 + + + +Title-Count: Three +Title-Type: Vocal Quartets +Title-Opus: 64 + + + +Title-RAW: Neue Liebeslieder - 15 Waltzes +Title-Opus: 65 + + + +Title-Count: Five +Title-Type: Duets +Title-Opus: 66 + + + +Title-Type: String Quartet +Title-No: 3 +Title-Key: B flat major +Title-Opus: 67 +Title-Dates: 1876 + + + +Title-Type: Symphony +Title-No: 1 +Title-Key: C minor +Title-Opus: 68 +Title-Dates: 1876 première + + + +Title-Count: Nine +Title-Type: Songs +Title-Opus: 69 + + + +Title-Count: Four +Title-Type: Songs +Title-Opus: 70 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 71 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 72 + + + +Title-Type: Symphony +Title-No: 2 +Title-Key: D major +Title-Opus: 73 +Title-Dates: 1877 + + + +Title-Count: Two +Title-Type: Motets +Title-Opus: 74 + + + +Title-Count: Four +Title-Type: Ballads and Romances +Title-Opus: 75 + + + +Title-Count: Eight +Title-Type: Pieces +Title-For: piano +Title-Opus: 76 +Title-Dates: 1878 + + + +Title-Type: Violin Concerto +Title-Key: D major +Title-Opus: 77 +Title-Dates: 1878 + + + +Title-Type: Violin Sonata +Title-No: 1 +Title-Key: G major +Title-Opus: 78 + + + +Title-Count: Two +Title-Type: Rhapsodies +Title-For: piano +Title-Opus: 79 +Title-Dates: 1879 + + + +Title-Name: Academic Festival Overture +Title-For: orchestra +Title-Opus: 80 +Title-Dates: 1880 + + + +Title-Name: Tragic Overture +Title-For: orchestra +Title-Opus: 81 +Title-Dates: 1880 + + + +Title-Name: Nänie +Title-Opus: 82 +Title-Dates: 1881 + + + +Title-Type: Piano Concerto +Title-No: 2 +Title-Key: B flat major +Title-Opus: 83 +Title-Dates: 1881 + + + +Title-Type: Romances and Songs +Title-Opus: 84 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 85 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 86 + + + +Title-Type: Piano Trio +Title-No: 2 +Title-Key: C major +Title-Opus: 87 + + + +Title-Type: String Quintet +Title-No: 1 +Title-Key: F major +Title-Opus: 88 +Title-Dates: 1882 + + + +Title-Name: Gesang der Parzen +Title-Opus: 89 + + + +Title-Type: Symphony +Title-No: 3 +Title-Key: F major +Title-Opus: 90 +Title-Dates: 1883 + + + +Title-Count: Two +Title-Type: Songs +Title-For: Voice, Viola & Piano +Title-Opus: 91 + + + +Title-Count: Four +Title-Type: Vocal Quartets +Title-Opus: 92 + + + +Title-Count: Six +Title-Type: Songs and Romances +Title-For: choir +Title-Opus: 93 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 94 + + + +Title-Count: Seven +Title-Type: Songs +Title-Opus: 95 + + + +Title-Count: Four +Title-Type: Songs +Title-Opus: 96 + + + +Title-Count: Six +Title-Type: Songs +Title-Opus: 97 + + + +Title-Type: Symphony +Title-No: 4 +Title-Key: E minor +Title-Opus: 98 +Title-Dates: 1885 + + + +Title-Type: Cello Sonata +Title-No: 2 +Title-Key: F major +Title-Opus: 99 +Title-Dates: 1886 + + + +Title-Type: Violin Sonata +Title-No: 2 +Title-Key: A major +Title-Opus: 100 +Title-Dates: 1886 + + + +Title-Type: Piano Trio +Title-No: 3 +Title-Key: C minor +Title-Opus: 101 +Title-Dates: 1886 + + + +Title-Type: Double Concerto +Title-For: Violin and Cello +Title-Key: A minor +Title-Opus: 102 +Title-Dates: 1887 + + + +Title-Name: Zigeunerlieder +Title-Opus: 103 + + + +Title-Count: Five +Title-Type: songs +Title-For: choir +Title-Opus: 104 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 105 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 106 + + + +Title-Count: Five +Title-Type: Songs +Title-Opus: 107 + + + +Title-Type: Violin Sonata +Title-No: 3 +Title-Key: D minor +Title-Opus: 108 + + + +Title-Name: Fest- und Gedenksprüche +Title-For: choir +Title-Opus: 109 + + + +Title-Count: Three +Title-Type: Motets +Title-Opus: 110 + + + +Title-Type: String Quintet +Title-No: 2 +Title-Key: G major +Title-Name: Prater +Title-Opus: 111 +Title-Dates: 1890 + + + +Title-Count: Six +Title-Type: Vocal Quartets +Title-Opus: 112 + + + +Title-Count: Thirteen +Title-Type: Canons +Title-For: female choir +Title-Opus: 113 + + + +Title-Type: Trio +Title-For: Piano, Clarinet, and Cello +Title-Key: A minor +Title-Opus: 114 +Title-Dates: 1891 + + + +Title-Type: Quintet +Title-For: Clarinet and Strings +Title-Key: B minor +Title-Opus: 115 +Title-Dates: 1891 + + + +Title-Count: Seven +Title-Type: Fantasias +Title-For: piano +Title-Opus: 116 +Title-Dates: 1892 + + + +Title-Count: Three +Title-Type: Intermezzi +Title-For: piano +Title-Opus: 117 +Title-Dates: 1892 + + + +Title-Count: Six +Title-Type: Pieces +Title-For: Piano +Title-Opus: 118 +Title-Dates: 1893 + + + +Title-Count: Four +Title-Type: Pieces +Title-For: piano +Title-Opus: 119 +Title-Dates: 1893 + + + +Title-Count: Two +Title-Type: Clarinet Sonatas +Title-Opus: 120 + + + +Title-Name: Vier ernste Gesänge +Title-RAW: ("Four Serious Songs") +Title-Opus: 121 +Title-Dates: 1896 + + + +Title-Count: Eleven +Title-Type: Chorale Preludes +Title-For: organ +Title-Opus: 122 +Title-Dates: 1896 + + + +Title-RAW: Hungarian Dances (1869) (Brahms considered these adaptations, not original works, and so he did not assign an Opus #) [1] +Title-Opus: WoO 1 + + + +Title-RAW: Chorale Prelude and Fugue on „O Traurigkeit, o Herzeleid“ +Title-For: organ +Title-Opus: WoO 7 + + + +Title-Type: Fugue +Title-Key: A flat minor +Title-For: organ +Title-Opus: WoO 8 + + + +Title-Type: Prelude and Fugue +Title-Key: A minor +Title-For: organ +Title-Opus: WoO 9 + + + +Title-Type: Prelude and Fugue +Title-Key: G minor +Title-For: organ +Title-Opus: WoO 10 + + + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/L_van_Beethoven.comp b/fhem/FHEM/lib/Normalize/Text/Music_Fields/L_van_Beethoven.comp new file mode 100644 index 000000000..aad1e4668 --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/L_van_Beethoven.comp @@ -0,0 +1,4227 @@ +# format = mail-header + + + + +## Format: "title; opus (date)" +## The dates are taken from CODM, beethovenlv, or wikipedia +## The most expanded date is taken; if there is a conflict, the first +## available date (in the order above) is taken + + + +# dup_opus_rex ^(?i)WoO\s+(15|74|105|116|158)\b + +# opus_prefix Op. + + + +## These titles are taken from wikipedia site: + +Title-Count: Three +Title-Type: Piano Trios +Title-Opus: 1 +Title-Dates: 1792--1794 + +Title-Type: Piano Trio +Title-No: 1 +Title-Key: E flat major +Title-Opus: 1-1 +Title-Dates: 1792--1793 + +Title-Type: Piano Trio +Title-No: 2 +Title-Key: G major +Title-Opus: 1-2 +Title-Dates: 1792--1794 + +Title-Type: Piano Trio +Title-No: 3 +Title-Key: C minor +Title-Opus: 1-3 +Title-Dates: 1792--1794 + +Title-Count: Three +Title-Type: Piano Sonatas +Title-Opus: 2 +Title-Dates: 1794--1795 + +Title-Type: Piano Sonata +Title-No: 1 +Title-Key: F minor +Title-Opus: 2-1 +Title-Dates: 1793--1795 + +Title-Type: Piano Sonata +Title-No: 2 +Title-Key: A major +Title-Opus: 2-2 +Title-Dates: 1794--1795 + +Title-Type: Piano Sonata +Title-No: 3 +Title-Key: C major +Title-Opus: 2-3 +Title-Dates: 1794--1795 + +Title-Type: String Trio +Title-No: 1 +Title-Key: E flat major +Title-Opus: 3 +Title-Dates: pre-1794 + +Title-Type: String Quintet +Title-Key: E flat major +Title-Opus: 4 +Title-Dates: 1795 + +Title-Count: Two +Title-Type: Cello Sonatas +Title-Opus: 5 +Title-Dates: 1796 + +Title-Type: Sonata +Title-For: Piano and Cello +Title-No: 1 +Title-Key: F major +Title-Opus: 5-1 +Title-Dates: 1796 + +Title-Type: Sonata +Title-For: Piano and Cello +Title-No: 2 +Title-Key: G minor +Title-Opus: 5-2 +Title-Dates: 1796 + +Title-Type: Piano Sonata +Title-For: four hands +Title-Opus: 6 +Title-Dates: 1797 + +Title-Type: Piano Sonata +Title-No: 4 +Title-Key: E flat major +Title-Opus: 7 +Title-Dates: 1796 + +Title-Type: Serenade +Title-Key: D major +Title-For: string trio +Title-Opus: 8 +Title-Dates: 1797 + +Title-Count: Three +Title-Type: String Trios +Title-Opus: 9 +Title-Dates: 1798 + +Title-Type: String Trio +Title-No: 2 +Title-Key: G major +Title-Opus: 9-1 +Title-Dates: 1798 + +Title-Type: String Trio +Title-No: 3 +Title-Key: D major +Title-Opus: 9-2 +Title-Dates: 1798 + +Title-Type: String Trio +Title-No: 4 +Title-Key: C minor +Title-Opus: 9-3 +Title-Dates: 1798 + +Title-Count: Three +Title-Type: Piano Sonatas +Title-Opus: 10 +Title-Dates: 1798 + +Title-Type: Piano Sonata +Title-No: 5 +Title-Key: C minor +Title-Opus: 10-1 +Title-Dates: 1795--1797 + +Title-Type: Piano Sonata +Title-No: 6 +Title-Key: F major +Title-Opus: 10-2 +Title-Dates: 1796--1797 + +Title-Type: Piano Sonata +Title-No: 7 +Title-Key: D major +Title-Opus: 10-3 +Title-Dates: 1797--1798 + +Title-Type: Piano Trio +Title-No: 4 +Title-Key: B flat major +Title-Opus: 11 +Title-Dates: 1797 + +Title-Count: Three +Title-Type: Violin Sonatas +Title-Opus: 12 +Title-Dates: 1798 + +Title-Type: Violin Sonata +Title-No: 1 +Title-Key: D major +Title-Opus: 12-1 +Title-Dates: 1798 + +Title-Type: Violin Sonata +Title-No: 2 +Title-Key: A major +Title-Opus: 12-2 +Title-Dates: 1798 + +Title-Type: Violin Sonata +Title-No: 3 +Title-Key: E flat major +Title-Opus: 12-3 +Title-Dates: 1798 + +Title-Type: Piano Sonata +Title-No: 8 +Title-Key: C minor +Title-Name: Pathetique +Title-Opus: 13 +Title-Dates: 1799 + +Title-Count: Two +Title-Type: Piano Sonatas +Title-Opus: 14 +Title-Dates: 1799 + +Title-Type: Piano Sonata +Title-No: 9 +Title-Key: E major +Title-Opus: 14-1 +Title-Dates: 1798--1799 + +Title-Type: Piano Sonata +Title-No: 10 +Title-Key: G major +Title-Opus: 14-2 +Title-Dates: 1799 + +Title-Type: Piano Concerto +Title-No: 1 +Title-Key: C major +Title-Punct: ; +Title-Comment: Op. 15 (comp. 1795--1798, f.p. (presumed) Vienna, 1800-4-2, soloist Beethoven, cond. Wranitzky; pubd. 1801-3) + +Title-Type: Quintet +Title-For: Piano and Winds +Title-Opus: 16 +Title-Dates: 1796, pubd. 1801 + +Title-Type: Horn Sonata +Title-Key: F major +Title-Opus: 17 +Title-Dates: 1800 + +Title-Count: Six +Title-Type: String Quartets +Title-Opus: 18 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 1 +Title-Key: F major +Title-Opus: 18-1 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 2 +Title-Key: G major +Title-Opus: 18-2 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 3 +Title-Key: D major +Title-Opus: 18-3 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 4 +Title-Key: C minor +Title-Opus: 18-4 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 5 +Title-Key: A major +Title-Opus: 18-5 +Title-Dates: 1800 + +Title-Type: String Quartet +Title-No: 6 +Title-Key: B flat major +Title-Opus: 18-6 +Title-Dates: 1800 + +Title-Type: Piano Concerto +Title-No: 2 +Title-Key: B flat major +Title-Opus: 19 +Title-Dates: comp. 1794--1795, f.p. Vienna, 1795-03-29, soloist Beethoven; pubd. 1801-12 + +Title-Type: Septet +Title-Key: E flat major +Title-Opus: 20 +Title-Dates: 1799 + +Title-Type: Symphony +Title-No: 1 +Title-Key: C major +Title-Opus: 21 +Title-Dates: comp. 1799--1800, f.p. Vienna, 1800-04-02, cond. P. Wranitzky; pubd. 1801 + +Title-Type: Piano Sonata +Title-No: 11 +Title-Key: B flat major +Title-Opus: 22 +Title-Dates: 1800 + +Title-Type: Violin Sonata +Title-No: 4 +Title-Key: A minor +Title-Opus: 23 +Title-Dates: 1800 + +Title-Type: Violin Sonata +Title-No: 5 +Title-Key: F major +Title-Name: Spring +Title-Opus: 24 +Title-Dates: 1801 + +Title-Type: Serenade +Title-Key: D major +Title-For: Flute, Violin and Viola +Title-Opus: 25 +Title-Dates: 1801 + +Title-Type: Piano Sonata +Title-No: 12 +Title-Key: A flat major +Title-Opus: 26 +Title-Dates: 1801 + +Title-Count: Two +Title-Type: Piano Sonatas +Title-Opus: 27 +Title-Dates: 1801 + +Title-Type: Piano Sonata +Title-No: 13 +Title-Key: E flat major +Title-Opus: 27-1 +Title-Dates: 1801 + +Title-Type: Piano Sonata +Title-No: 14 +Title-Key: C sharp minor +Title-Name: Moonlight +Title-Opus: 27-2 +Title-Dates: 1800--1801 + +Title-Type: Piano Sonata +Title-No: 15 +Title-Key: D major +Title-Opus: 28 +Title-Dates: 1801 + +Title-Type: String Quintet +Title-Key: C major +Title-Opus: 29 +Title-Dates: 1801 + +Title-Count: Three +Title-Type: Violin Sonatas +Title-Opus: 30 +Title-Dates: 1801--1802 + +Title-Type: Violin Sonata +Title-No: 6 +Title-Key: A major +Title-Opus: 30-1 +Title-Dates: 1801--1802 + +Title-Type: Violin Sonata +Title-No: 7 +Title-Key: C minor +Title-Opus: 30-2 +Title-Dates: 1801--1802 + +Title-Type: Violin Sonata +Title-No: 8 +Title-Key: G major +Title-Opus: 30-3 +Title-Dates: 1801--1802 + +Title-Count: Three +Title-Type: Piano Sonatas +Title-Opus: 31 +Title-Dates: 1802 + +Title-Type: Piano Sonata +Title-No: 16 +Title-Key: G major +Title-Opus: 31-1 +Title-Dates: 1802 + +Title-Type: Piano Sonata +Title-No: 17 +Title-Key: D minor +Title-Name: Tempest +Title-Opus: 31-2 +Title-Dates: 1802 + +Title-Type: Piano Sonata +Title-No: 18 +Title-Key: E flat major +Title-Name: The Hunt +Title-Opus: 31-3 +Title-Dates: 1802 + +Title-RAW: Song - An die Hoffnung +Title-Opus: 32 +Title-Dates: 1805 + +Title-Count: Seven +Title-Type: Bagatelles +Title-For: piano +Title-Opus: 33 +Title-Dates: 1802 + +Title-Count: Six +Title-Type: variations +Title-For: piano +Title-Related-On: an original theme +Title-Key: F major +Title-Opus: 34 +Title-Dates: 1802 + +Title-RAW: Fifteen variations and a fugue for piano on an original theme +Title-Key: E flat major +Title-Name: Eroica +Title-Opus: 35 +Title-Dates: 1802 + +Title-Type: Symphony +Title-No: 2 +Title-Key: D major +Title-Opus: 36 +Title-Dates: comp. 1801--1802, f.p. Vienna, 1803-04-05, cond. Beethoven; pubd. 1804 + +Title-Type: Piano Concerto +Title-No: 3 +Title-Key: C minor +Title-Opus: 37 +Title-Dates: comp. 1800--1801, f.p. Vienna, 1803-04-05, soloist Beethoven; pubd. 1804 + +Title-RAW: Piano Trio No. 8 (Arrangement of the Septet; Op. 20)) +Title-Opus: 38 +Title-Dates: 1820--1823 + +Title-RAW: Two Preludes through all twelve major keys +Title-For: piano +Title-Opus: 39 +Title-Dates: 1789 + +Title-Type: Romance +Title-For: Violin +Title-Key: G major +Title-Opus: 40 +Title-Dates: 1802 + +Title-Type: Serenade +Title-For: Piano and Flute or Violin +Title-Key: D major +Title-Opus: 41 +Title-Dates: 1803 + +Title-Type: Notturno +Title-For: Viola and Piano +Title-Key: D major +Title-Opus: 42 +Title-Dates: 1803 + +Title-RAW: The Creatures of Prometheus, Overture and Ballet music +Title-Opus: 43 +Title-Dates: 1801 + +Title-RAW: Piano Trio No. 10 (Variations on an original theme in E flat major) +Title-Opus: 44 +Title-Dates: 1802--1803 + +Title-Count: Three +Title-Type: Marches +Title-For: Piano, 4 hands +Title-Opus: 45 +Title-Dates: 1803 + +Title-RAW: Song - Adelaide +Title-Opus: 46 +Title-Dates: 1795 + +Title-Type: Violin Sonata +Title-No: 9 +Title-Key: A major +Title-Name: Kreutzer +Title-Opus: 47 +Title-Dates: 1802 + +Title-Count: Six +Title-Type: Songs +Title-Opus: 48 +Title-Dates: 1802 + +Title-RAW: Bitten +Title-Opus: 48-1 +Title-Dates: 1802 + +Title-RAW: Die Liebe des Nächsten +Title-Opus: 48-2 +Title-Dates: 1802 + +Title-RAW: Vom Tode +Title-Opus: 48-3 +Title-Dates: 1802 + +Title-RAW: Die Ehre Gottes aus der Natur +Title-Opus: 48-4 +Title-Dates: 1802 + +Title-RAW: Gottes Macht und Vorsehung, Bußlied +Title-Opus: 48-5 +Title-Dates: 1802 + +Title-Count: Two +Title-Type: Piano Sonatas +Title-Opus: 49 +Title-Dates: 1802 + +Title-Type: Piano Sonata +Title-No: 19 +Title-Key: G minor +Title-Opus: 49-1 +Title-Dates: 1797 + +Title-Type: Piano Sonata +Title-No: 20 +Title-Key: G major +Title-Opus: 49-2 +Title-Dates: 1795--1796 + +Title-Type: Romance +Title-For: Violin +Title-Key: F major +Title-Opus: 50 +Title-Dates: 1798 + +Title-Count: Two +Title-Type: Rondos +Title-For: Piano +Title-Opus: 51 +Title-Dates: 1797 + +Title-Type: Rondo +Title-Key: C major +Title-Opus: 51-1 +Title-Dates: 1796--1797 + +Title-Type: Rondo +Title-Key: G major +Title-Opus: 51-2 +Title-Dates: 1798 + +Title-Count: Eight +Title-Type: Songs +Title-Opus: 52 +Title-Dates: 1785--1793 + +Title-RAW: Urians Reise um die Welt +Title-Opus: 52-1 +Title-Dates: 1785--1793 + +Title-RAW: Feuerfab +Title-Opus: 52-2 +Title-Dates: 1785--1793 + +Title-RAW: Das Liedchen von der Ruhe +Title-Opus: 52-3 +Title-Dates: 1785--1793 + +Title-RAW: Maigesang +Title-Opus: 52-4 +Title-Dates: 1785--1793 + +Title-RAW: Mollys Abschied +Title-Opus: 52-5 +Title-Dates: 1785--1793 + +Title-RAW: Die Liebe +Title-Opus: 52-6 +Title-Dates: 1785--1793 + +Title-RAW: Marmotte +Title-Opus: 52-7 +Title-Dates: 1785--1793 + +Title-RAW: Das Blümchen Wunderhold +Title-Opus: 52-8 +Title-Dates: 1785--1793 + +Title-Type: Piano Sonata +Title-No: 21 +Title-Key: C major +Title-Name: Waldstein +Title-Opus: 53 +Title-Dates: 1804 + +Title-Type: Piano Sonata +Title-No: 22 +Title-Key: F major +Title-Opus: 54 +Title-Dates: 1804 + +Title-Type: Symphony +Title-No: 3 +Title-Key: E flat major +Title-Name: Eroica +Title-Opus: 55 +Title-Dates: comp. 1803--1804, f.pub.p. Vienna, 1805-04-07; pubd. 1806 + +Title-Type: Triple Concerto +Title-Key: C major +Title-Opus: 56 +Title-Dates: comp. 1804, f.p. 1808; pubd. 1807 + +Title-Type: Piano Sonata +Title-No: 23 +Title-Key: F minor +Title-Name: Appassionata +Title-Opus: 57 +Title-Dates: 1805 + +Title-Type: Piano Concerto +Title-No: 4 +Title-Key: G major +Title-Opus: 58 +Title-Dates: comp. 1805--1806, f.p. Vienna, 1808-12-22, soloist Beethoven; pubd. 1808 + +Title-RAW: Three String Quartets, the Rasumovsky quartets +Title-Opus: 59 +Title-Dates: comp. 1806 + +Title-Type: String Quartet +Title-No: 7 +Title-Key: F major +Title-Opus: 59-1 +Title-Dates: comp. 1806 + +Title-Type: String Quartet +Title-No: 8 +Title-Key: E minor +Title-Opus: 59-2 +Title-Dates: comp. 1806 + +Title-Type: String Quartet +Title-No: 9 +Title-Key: C major +Title-Opus: 59-3 +Title-Dates: comp. 1806 + +Title-Type: Symphony +Title-No: 4 +Title-Key: B flat major +Title-Opus: 60 +Title-Dates: comp. 1806, f.pub.p. Vienna, 1807-11-15, cond. Clement; pubd. 1808 + +Title-Type: Concerto +Title-For: Violin and Orchestra +Title-Key: D major +Title-Opus: 61 +Title-Dates: arr. for pf. by Beethoven 1807, pubd. 1808 / comp. 1806, f.p. Vienna, 1806-12-23, soloist Franz Clement; pub. 1809 + +Title-RAW: Overture - Coriolan +Title-Opus: 62 +Title-Dates: 1807 + +Title-RAW: Arrangement of String Quintet (Op. 4) +Title-For: Piano Trio +Title-Opus: 63 +Title-Dates: 1806 + +Title-RAW: Arrangement of String Trio (Op. 3) +Title-For: Piano and Cello +Title-Opus: 64 +Title-Dates: 1807 + +Title-RAW: Aria - Ah perfido! +Title-Opus: 65 +Title-Dates: comp. 1796 + +Title-RAW: Variations for Cello on Mozart's Ein Mädchen oder Weibchen +Title-Opus: 66 +Title-Dates: 1796 + +Title-Type: Symphony +Title-No: 5 +Title-Key: C minor +Title-Opus: 67 +Title-Dates: comp. 1804--1808, f.p. Vienna, 1808-12-22, cond. Beethoven; pubd. 1809 + +Title-Type: Symphony +Title-No: 6 +Title-Key: F major +Title-Name: Pastoral +Title-Opus: 68 +Title-Dates: comp. 1807--1808, f.p. Vienna, 1808-12-22, cond. Beethoven; pubd. 1809 + +Title-Type: Sonata +Title-For: Piano and Violoncello +Title-No: 3 +Title-Key: A major +Title-Opus: 69 +Title-Dates: 1808 + +Title-Count: Two +Title-Type: Piano Trios +Title-Opus: 70 +Title-Dates: 1808 + +Title-Type: Piano Trio +Title-No: 5 +Title-Key: D major +Title-Name: Ghost +Title-Opus: 70-1 +Title-Dates: 1808 + +Title-Type: Piano Trio +Title-No: 6 +Title-Key: E flat major +Title-Opus: 70-2 +Title-Dates: 1808 + +Title-Type: Wind sextet +Title-Key: E flat major +Title-Opus: 71 +Title-Dates: 1796 + +Title-RAW: Opera - Fidelio (c. 1803-5; Fidelio Overture composed 1814) +Title-Opus: 72 +Title-Dates: 1805, rev. 1806 and 1814 / 1814 + +Title-RAW: Opera - Leonore (earlier version of Fidelio, with Leonore No. 2 Overture) +Title-Opus: 72a +Title-Dates: 1805 + +Title-RAW: Opera - Leonore (earlier version of Fidelio, with Leonore No. 3 Overture) +Title-Opus: 72b +Title-Dates: 1806 + +Title-Type: Piano Concerto +Title-No: 5 +Title-Key: E flat major +Title-Name: Emperor +Title-Opus: 73 +Title-Dates: comp. 1809, f.p. Leipzig, 1810-12, soloist F. Schneider, f. Vienna p. 1812-02-12, soloist Czerny; pubd. 1811 + +Title-Type: String Quartet +Title-No: 10 +Title-Key: E flat major +Title-Name: Harp +Title-Opus: 74 +Title-Dates: 1809 + +Title-Count: Six +Title-Type: Songs +Title-Opus: 75 +Title-Dates: 1809 + +Title-Type: Mignon +Title-Opus: 75-1 +Title-Dates: 1809 + +Title-RAW: Neue Liebe neues Leben +Title-Opus: 75-2 +Title-Dates: 1809 + +Title-RAW: Aus Goethes Faust: Es war einmal ein König +Title-Opus: 75-3 +Title-Dates: 1809 + +Title-RAW: Gretels Warnung +Title-Opus: 75-4 +Title-Dates: 1809 + +Title-RAW: An die fernen Geliebten +Title-Opus: 75-5 +Title-Dates: 1809 + +Title-RAW: Der Zufriedene +Title-Opus: 75-6 +Title-Dates: 1809 + +Title-Count: Six +Title-Type: variations +Title-For: piano +Title-Related-On: an original theme +Title-Key: D major +Title-Opus: 76 +Title-Dates: 1810 + +Title-Type: Piano Fantasia +Title-Opus: 77 +Title-Dates: 1810 + +Title-Type: Piano Sonata +Title-No: 24 +Title-Key: F sharp major +Title-Opus: 78 +Title-Dates: 1809 + +Title-Type: Piano Sonata +Title-No: 25 +Title-Key: G major +Title-Opus: 79 +Title-Dates: 1809 + +Title-Type: Fantasy +Title-Key: C minor +Title-For: piano, chorus, and orchestra +Title-Opus: 80 +Title-Dates: 1808 + +Title-Type: Piano Sonata +Title-No: 26 +Title-Key: E flat major +Title-Name: Les Adieux +Title-Opus: 81a +Title-Dates: 1809 + +Title-Type: Sextet +Title-Key: E flat major +Title-Opus: 81b +Title-Dates: ?1795 + +Title-RAW: Four Ariettas and a Duet +Title-Opus: 82 +Title-Dates: 1809 + +Title-RAW: Dimmi, ben mio, che m'ami +Title-Opus: 82-1 +Title-Dates: 1809 + +Title-RAW: T'intendo si, mio cor +Title-Opus: 82-2 +Title-Dates: 1809 + +Title-RAW: L'amante impaziente (first version) +Title-Opus: 82-3 +Title-Dates: 1809 + +Title-RAW: L'amante impatiente (second version) +Title-Opus: 82-4 +Title-Dates: 1809 + +Title-RAW: Duet: Odi 'laura che dolce sospira +Title-Opus: 82-5 +Title-Dates: 1809 + +Title-Count: Three +Title-Type: Songs +Title-Opus: 83 +Title-Dates: 1810 + +Title-RAW: Wonne der Wehmut +Title-Opus: 83-1 +Title-Dates: 1810 + +Title-RAW: Sehnsucht +Title-Opus: 83-2 +Title-Dates: 1810 + +Title-RAW: Mit einem gemalten Band +Title-Opus: 83-3 +Title-Dates: 1810 + +Title-RAW: Egmont (Overture and Incidental Music) +Title-Opus: 84 +Title-Dates: 1810 + +Title-RAW: Christus am Ölberge or Christ on the Mount of Olives +Title-Opus: 85 +Title-Dates: 1803 + +Title-Type: Mass +Title-Key: C major +Title-Opus: 86 +Title-Dates: 1807 + +Title-Type: Trio +Title-For: two Oboes and English Horn +Title-Key: C major +Title-Opus: 87 +Title-Dates: 1795 + +Title-RAW: Song - Das Gluck der Freundschaft +Title-Opus: 88 +Title-Dates: 1803 + +Title-Type: Polonaise +Title-Key: C major +Title-Opus: 89 +Title-Dates: 1814 + +Title-Type: Piano Sonata +Title-No: 27 +Title-Key: E minor +Title-Opus: 90 +Title-Dates: 1814 + +Title-RAW: Wellington's Victory ("Battle" Symphony) +Title-Opus: 91 +Title-Dates: comp. 1813, f.p. Vienna, 1813-12-08, cond. Beethoven; pubd. 1816 + +Title-Type: Symphony +Title-No: 7 +Title-Key: A major +Title-Opus: 92 +Title-Dates: comp. 1811--1812, f.p. Vienna, 1813-12-08, cond. Beethoven; pubd. 1816 + +Title-Type: Symphony +Title-No: 8 +Title-Key: F major +Title-Opus: 93 +Title-Dates: comp. 1812, f.p. Vienna, 1814-02-27, cond. Beethoven; pubd. 1816 + +Title-RAW: Song - An die Hoffnung +Title-Opus: 94 +Title-Dates: 1813--1815 + +Title-Type: String Quartet +Title-No: 11 +Title-Key: F minor +Title-Name: Serioso +Title-Opus: 95 +Title-Dates: 1810 + +Title-Type: Violin Sonata +Title-No: 10 +Title-Key: G major +Title-Opus: 96 +Title-Dates: 1812, rev. 1815 + +Title-Type: Piano Trio +Title-No: 7 +Title-Key: B flat major +Title-Name: Archduke +Title-Opus: 97 +Title-Dates: 1810--1811 + +Title-RAW: Song Cycle - An die ferne Geliebte +Title-Opus: 98 +Title-Dates: 1816 + +Title-RAW: Song - Der Mann von Wort +Title-Opus: 99 +Title-Dates: 1816 + +Title-RAW: Song - Merkenstein +Title-Opus: 100 +Title-Dates: 1815 + +Title-Type: Piano Sonata +Title-No: 28 +Title-Key: A major +Title-Opus: 101 +Title-Dates: 1816 + +Title-Count: Two +Title-Type: Cello Sonatas +Title-Opus: 102 +Title-Dates: 1815 + +Title-Type: Sonata +Title-For: Piano and Cello +Title-No: 4 +Title-Key: C major +Title-Opus: 102-1 +Title-Dates: 1815 + +Title-Type: Sonata +Title-For: Piano and Cello +Title-No: 5 +Title-Key: D minor +Title-Opus: 102-2 +Title-Dates: 1815 + +Title-Type: Wind octet +Title-Key: E flat major +Title-Opus: 103 +Title-Dates: 1792 + +Title: String Quintet (arrangement of Piano Trio No. 3, 1817); Op. 104 (arr. by Beethoven in 1817 of his pf. trio (1792-4)) + +Title-Count: Six +Title-Type: sets of variations +Title-For: Piano and Flute +Title-Opus: 105 +Title-Dates: 1819 + +Title-Type: Piano Sonata +Title-No: 29 +Title-Key: B flat major +Title-Name: Hammerklavier +Title-Opus: 106 +Title-Dates: 1818 + +Title-Count: Ten +Title-Type: sets of variations +Title-For: Piano and Flute +Title-Opus: 107 +Title-Dates: 1820 + +Title-RAW: Twenty-Five Scottish Songs +Title-Opus: 108 +Title-Dates: 1815-1816 + +Title-Type: Piano Sonata +Title-No: 30 +Title-Key: E major +Title-Opus: 109 +Title-Dates: 1820 + +Title-Type: Piano Sonata +Title-No: 31 +Title-Key: A flat major +Title-Opus: 110 +Title-Dates: 1821 + +Title-Type: Piano Sonata +Title-No: 32 +Title-Key: C minor +Title-Opus: 111 +Title-Dates: 1822 + +Title-RAW: Meeresstille und glückliche Fahrt (for chorus and orchestra) +Title-Opus: 112 +Title-Dates: 1815 + +Title-RAW: Overture and incidental music for Die Ruinen von Athen (The ruins of Athens) +Title-Opus: 113 +Title-Dates: 1811 + +Title-RAW: March and Chorus - Die Weihe des Hauses ("The Consecration of the House"; 1822) +Title-Opus: 114 +Title-Dates: 1822 + +Title-RAW: Overture - Zur Namensfeier +Title-Opus: 115 +Title-Dates: 1815 + +Title-RAW: Vocal Trio with Orchestra - Tramte, empi tremate +Title-Opus: 116 +Title-Dates: 1802 + +Title-RAW: Overture to King Stephen +Title-Opus: 117 +Title-Dates: 1811 + +Title-RAW: Elegischer Gesang (for chorus and orchestra) +Title-Opus: 118 +Title-Dates: 1814 + +Title-RAW: Eleven new Bagatelles +Title-For: piano +Title-Opus: 119 +Title-Dates: 1821 + +Title-RAW: Thirty-three variations for piano on a waltz by Diabelli, C major (Diabelli Variations) +Title-Opus: 120 +Title-Dates: 1823 + +Title-RAW: Piano Trio No. 11 (Variations on Ich bin der Schneider Kakadu) +Title-Opus: 121 +Title-Dates: 1803 + +Title-RAW: Opferlied (for chorus and orchestra) +Title-Opus: 121b +Title-Dates: 1822 + +Title-RAW: Bundeslied (for chorus and orchestra) +Title-Opus: 122 +Title-Dates: 1824 + +Title-RAW: Mass in D major (Missa Solemnis) +Title-Opus: 123 +Title-Dates: 1819--1822 + +Title-RAW: Overture - Die Weihe des Hauses (Consecration of the House) +Title-Opus: 124 +Title-Dates: 1822 + +Title-Type: Symphony +Title-No: 9 +Title-Key: D minor +Title-Name: Choral +Title-Opus: 125 +Title-Dates: comp. 1817--1823, f.p. Vienna, 1824-05-07, cond. Beethoven; pubd. 1826 + +Title-Count: Six +Title-Type: Bagatelles +Title-For: piano +Title-Opus: 126 +Title-Dates: 1824 + +Title-Type: String Quartet +Title-No: 12 +Title-Key: E flat major +Title-Opus: 127 +Title-Dates: 1825 + +Title-RAW: Song - Der Kuss +Title-Opus: 128 +Title-Dates: 1822 + +Title-RAW: Rondo Capriccio for piano in G major (Rage over a lost penny) +Title-Opus: 129 +Title-Dates: 1825--1826 + +Title-Type: String Quartet +Title-No: 13 +Title-Key: B flat major +Title-Opus: 130 +Title-Dates: 1825 + +Title-Type: String Quartet +Title-No: 14 +Title-Key: C sharp minor +Title-Opus: 131 +Title-Dates: 1826 + +Title-Type: String Quartet +Title-No: 15 +Title-Key: A minor +Title-Opus: 132 +Title-Dates: 1825 + +Title-RAW: Große Fuge +Title-Key: B flat major +Title-For: string quartet +Title-Opus: 133 +Title-Dates: 1825 + +Title-RAW: Piano arrangement (4 hands) of Große Fuge +Title-Opus: 134 +Title-Dates: 1826 + +Title-Type: String Quartet +Title-No: 16 +Title-Key: F major +Title-Opus: 135 +Title-Dates: 1826 + +Title-RAW: Cantata - Der glorreiche Augenblick +Title-Opus: 136 +Title-Dates: 1814 + +Title-RAW: String Quintet (fugue) +Title-Key: D major +Title-Opus: 137 +Title-Dates: 1817 + +Title-RAW: Opera - Leonore (earlier version of Fidelio, with Leonore No. 1 Overture) +Title-Opus: 138 +Title-Dates: 1807 + +## Music for a Ritterballett; WoO 1 (1790--1791) +## Triumphal March for orchestra for Christoph Kuffner's tragedy Tarpeja; WoO 2a +## Prelude to Act II of Tarpeja; WoO 2b +## "Gratulations-Menuett", minuet for orchestra; WoO 3 (1822) +## Piano Concerto in E flat major (solo part only with indications of orchestration); WoO 4 (1784) +## Violin Concerto movement in C major, fragment; WoO 5 (1790--1792) +## Rondo in B flat major for piano and orchestra, fragment, possibly part of initial version of the Piano Concerto No. 2; WoO 6 (1793) +## Twelve minuets for orchestra; WoO 7 (1795) +## Twelve German Dances for orchestra (later arranged for piano); WoO 8 (1795) +## Six minuets for two violins and cello; WoO 9 (1795) +## Six minuets for orchestra (original version lost, only an arrangement for piano is extant); WoO 10 (1795) +## Seven Ländler for two violins and cello (original version lost, only an arrangement for piano is extant); WoO 11 (1799) +## Twelve minuets for orchestra (probably spurious, actually by Beethoven's brother Carl); WoO 12 (1799) +## Twelve German Dances for orchestra (only a version for piano is extant); WoO 13 (1792--1797) +## Twelve contredanses for orchestra; WoO 14 +## Six Laendler for two violins and cello (also arranged for piano); WoO 15 (1802) +## Twelve Ecossaises for orchestra (probably spurious); WoO 16 +## Eleven "Mödlinger Tänze" for seven instruments (probably spurious); WoO 17 (1819) +## March for Military Band (trio added later); WoO 18 (1809--1810) +## March for Military Band (trio added later); WoO 19 (1810) +## March for Military Band (trio added later); WoO 20 (1809--1822) +## Polonaise for Military Band; WoO 21 (1810) +## Ecossaise for Military Band; WoO 22 (1809--1810) +## Ecossaise for Military Band (only a piano arrangement by Carl Czerny is extant); WoO 23 (1810) +## March for Military Band; WoO 24 (1816) +## Rondo for two oboes, two clarinets, two French horns and two bassoons (original finale of the Octet, opus 103; WoO 25 (1792--1793) +## Duo for two flutes; WoO 26 (1792) +## Three duets for clarinet and bassoon (possibly spurious); WoO 27 +## Variations for two oboes and cor anglais on "Là ci darem la mano" from Wolfgang Amadeus Mozart's opera Don Giovanni; WoO 28 (1795) +## March for wind; WoO 29 (1797--1798) +## Three Equali for four trombones (also arranged for four male voices); WoO 30 (1812) +## Fugue for organ; WoO 31 (1783) +## Duo for viola and cello, "mit zwei obligaten Augengläsern" ("with two obbligato eyeglasses"); WoO 32 (1796--1797) +## Five pieces for mechanical clock or flute; WoO 33 +## Duet for two violins; WoO 34 (1822) +## Canon for two violins; WoO 35 (1825) +## Three piano quartets; WoO 36 (1785) +## Trio in G major for piano, flute and bassoon; WoO 37 (1786) +## Piano Trio No. 8 in E flat major; WoO 38 (1785--1791) +## Allegretto in B flat major for piano trio; WoO 39 (1812) +## Twelve variations for piano and violin on "Se vuol ballare" from Mozart's The Marriage of Figaro; WoO 40 (1792--1793) +## Rondo in G major for piano and violin; WoO 41 (1793--1794) +## Six German Dances for violin and piano; WoO 42 (1796) +## Sonatina for mandolin and harpsichord; WoO 43a +## Adagio for mandolin and harpsichord; WoO 43b +## Sonatina for mandolin and piano; WoO 44a +## Andante and variations for mandolin and harpsichord; WoO 44b +## Twelve variations for cello and piano "See the conqu'ring hero comes" from George Frideric Händel's oratorio Judas Maccabaeus; WoO 45 (1796) +## Seven variations for cello and piano on "Bei Männern, welche Liebe fühlen" from Mozart's opera Die Zauberflöte; WoO 46 (1801) +## Andante Favori - Original middle movement from Piano Sonata No. 21 (Waldstein); WoO 57 (1803--1804) +## Für Elise - Bagatelle in A minor for solo piano; WoO 59 (1808) +## String Quintet in C major (Fragment, Piano Transcription); WoO 62 (1826) +## Nine variations for piano on a march by Ernst Christoph Dressler; WoO 63 (1782) +## Six variations for piano or harp on a Swiss song; WoO 64 (1790--1792) +## Twenty-four variations for piano on Vincenzio Righini's aria "Venni Amore"; WoO 65 (1790--1791) +## Thirteen variations for piano on the aria "Es war einmal ein alter Mann" from Carl Ditters von Dittersdorf's opera Das rote Käppchen; WoO 66 (1792) +## Eight variations for piano four hands on a theme by Count Waldstein; WoO 67 (1792) +## Twelve variations for piano on the "Menuet a la Vigano" from Jakob Haibel's ballet La nozza disturbate; WoO 68 (1795) +## Nine variations for piano on "Quant'e piu bello" from Giovanni Paisiello's opera La Molinara; WoO 69 (1795) +## Six variations for piano on "Nel cor piu non mi sento" from Giovanni Paisiello's opera La Molinara; WoO 70 (1795) +## Twelve variations for piano on the Russian dance from Paul Wranitzky's ballet Das Waldmädchen; WoO 71 (1796--1797) +## Eight variations for piano on "Mich brennt ein heisses Fieber" from André-Ernest-Modeste Grétry's opera Richard Löwenherz; WoO 72 (1795--1798) +## Ten variations for piano on "La stessa, la stessissima" from Antonio Salieri's opera Falstaff; WoO 73 (1799) +## "Ich denke dein" - song with six variations for piano four hands; WoO 74 (1799--1803) +## Seven variations for piano on "Kind, willst du ruhig schlafen" from Peter Winter's opera Das unterbrochene Opferfest; WoO 75 (1792--1799) +## Eight variations for piano on "Tandeln und scherzen" from Franz Xaver Süssmayr's opera Soliman II; WoO 76 (1799) +## Six easy variations for piano on an original theme; WoO 77 (1800) +## Seven variations for piano on "God Save the King"; WoO 78 (1802--1803) +## Five variations for piano on "Rule Britannia"; WoO 79 (1803) +## Thirty-two variations in C minor on an original theme; WoO 80 (1806) + + + +## These titles are taken from beethovenlv site: + +Title-RAW: Ballet Music "Ritterballet", music for a ballet of knights +Title-Opus: WoO 1 +Title-Dates: 1790--1791 + +Title-RAW: Triumphal March +Title-For: Orchestra +Title-Key: C major +Title-Name: Tarpeja +Title-Opus: WoO 2-1 +Title-Dates: 1813 + +Title-RAW: Entr’acte +Title-Key: D major +Title-Name: Tarpeja +Title-Comment: (now thought to be from the opening of Act II from Leonore 1805) +Title-Opus: WoO 2-2 +Title-Dates: 1813 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: E flat major +Title-Name: Gratulations - Menuett +Title-Opus: WoO 3 +Title-Dates: 1822 + +Title-Type: Concerto +Title-For: Piano & Orchestra +Title-Key: E flat major +Title-Punct: ; +Title-Comment: authorship: reconstruction +Title-Opus: WoO 4 +Title-Dates: 1784 + +Title-Type: Concerto +Title-For: Violin & Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: fragment of first movement; three completions made; authorship: unfinished +Title-Opus: WoO 5 +Title-Dates: 1790--1792 + +Title-Type: Rondo +Title-For: Piano & Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: fragment, completed by Carl Czerny +Title-Opus: WoO 6 +Title-Dates: 1793 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 1 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-1 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 2 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-2 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 3 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-3 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 4 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-4 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 5 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-5 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 6 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-6 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 7 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-7 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 8 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-8 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 9 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-9 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 10 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-10 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 11 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-11 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: F major +Title-Punct: : +Title-Comment: No. 12 of 12 Minuets for Orchestra; piano version is Hess 101 +Title-Opus: WoO 7-12 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 1 of 12 German Dances +Title-Opus: WoO 8-1 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 2 of 12 German Dances +Title-Opus: WoO 8-2 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: F major +Title-Punct: : +Title-Comment: No. 3 of 12 German Dances +Title-Opus: WoO 8-3 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 4 of 12 German Dances +Title-Opus: WoO 8-4 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 5 of 12 German Dances +Title-Opus: WoO 8-5 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 6 of 12 German Dances +Title-Opus: WoO 8-6 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 7 of 12 German Dances +Title-Opus: WoO 8-7 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 8 of 12 German Dances +Title-Opus: WoO 8-8 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: F major +Title-Punct: : +Title-Comment: No. 9 of 12 German Dances +Title-Opus: WoO 8-9 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 10 of 12 German Dances +Title-Opus: WoO 8-10 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 11 of 12 German Dances +Title-Opus: WoO 8-11 +Title-Dates: 1795 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 12 of 12 German Dances +Title-Opus: WoO 8-12 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violins & Cello +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 1 of 6 Minuets +Title-Opus: WoO 9-1 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violins & Cello +Title-Key: G major +Title-Punct: : +Title-Comment: No. 2 of 6 Minuets +Title-Opus: WoO 9-2 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violins & Cello +Title-Key: C major +Title-Punct: : +Title-Comment: No. 3 of 6 Minuets +Title-Opus: WoO 9-3 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violins & Cello +Title-Key: F major +Title-Punct: : +Title-Comment: No. 4 of 6 Minuets +Title-Opus: WoO 9-4 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violin & Cello +Title-Key: D major +Title-Punct: : +Title-Comment: No. 5 of 6 Minuets +Title-Opus: WoO 9-5 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Violins & Cello +Title-Key: G major +Title-Punct: : +Title-Comment: No. 6 of 6 Minuets +Title-Opus: WoO 9-6 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 1 of 6 minuets; surviving piano version +Title-Opus: WoO 10-1 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 2 of 6 minuets; surviving piano version +Title-Opus: WoO 10-2 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 3 of 6 minuets; surviving piano version +Title-Opus: WoO 10-3 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 4 of 6 minuets; surviving piano version +Title-Opus: WoO 10-4 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 5 of 6 minuets; surviving piano version +Title-Opus: WoO 10-5 +Title-Dates: 1795 + +Title-Type: Minuet +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 6 of 6 minuets; surviving piano version +Title-Opus: WoO 10-6 +Title-Dates: 1795 + +Title-Count: 7 +Title-Type: Ländler +Title-For: Violins & Cello +Title-Key: D major +Title-Punct: : +Title-Comment: surviving piano version +Title-Opus: WoO 11 +Title-Dates: 1799 + +Title-RAW: 12 Minuets for Orchestra; authorship: not certain +Title-Opus: WoO 12 +Title-Dates: 1799 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 1 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-1 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 2 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-2 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 3 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-3 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 4 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-4 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: F major +Title-Punct: : +Title-Comment: No. 5 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-5 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 6 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-6 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 7 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-7 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 8 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-8 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 9 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-9 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 10 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-10 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 11 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-11 +Title-Dates: 1792--1797 + +Title-Type: Dance +Title-For: Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 12 of 12 German Dances; surviving piano version +Title-Opus: WoO 13-12 +Title-Dates: 1792--1797 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 1 of 12 Contredanses +Title-Opus: WoO 14-1 +Title-Dates: 1791--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 2 of 12 Contredanses +Title-Opus: WoO 14-2 +Title-Dates: 1801--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: D major +Title-Punct: : +Title-Comment: No. 3 of 12 Contredanses +Title-Opus: WoO 14-3 +Title-Dates: 1795--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 4 of 12 Contredanses +Title-Opus: WoO 14-4 +Title-Dates: 1795--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 5 of 12 Contredanses +Title-Opus: WoO 14-5 +Title-Dates: 1791--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 6 of 12 Contredanses +Title-Opus: WoO 14-6 +Title-Dates: 1795--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 7 of 12 Contredanses +Title-Opus: WoO 14-7 +Title-Dates: 1800--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 8 of 12 Contredanses +Title-Opus: WoO 14-8 +Title-Dates: 1791--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: A major +Title-Punct: : +Title-Comment: No. 9 of 12 Contredanses +Title-Opus: WoO 14-9 +Title-Dates: 1801--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: C major +Title-Punct: : +Title-Comment: No. 10 of 12 Contredanses +Title-Opus: WoO 14-10 +Title-Dates: 1801--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: G major +Title-Punct: : +Title-Comment: No. 11 of 12 Contredanses +Title-Opus: WoO 14-11 +Title-Dates: 1800--1802 + +Title-Type: Contredanse +Title-For: Small Orchestra +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 12 of 12 Contredanses +Title-Opus: WoO 14-12 +Title-Dates: 1791--1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D major +Title-Punct: : +Title-Comment: No. 1 of 6 ländlerische Tänze +Title-Opus: WoO 15-1 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: No. 1 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-1 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D major +Title-Punct: : +Title-Comment: No. 2 of 6 ländlerische Tänze +Title-Opus: WoO 15-2 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: No. 2 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-2 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D major +Title-Punct: : +Title-Comment: No. 3 of 6 ländlerische Tänze +Title-Opus: WoO 15-3 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: No. 3 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-3 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D minor +Title-Punct: : +Title-Comment: No. 4 of 6 ländlerische Tänze +Title-Opus: WoO 15-4 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D minor +Title-Punct: : +Title-Comment: No. 4 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-4 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D major +Title-Punct: : +Title-Comment: No. 5 of 6 ländlerische Tänze +Title-Opus: WoO 15-5 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: No. 5 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-5 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Violins & Bass +Title-Key: D major +Title-Punct: : +Title-Comment: No. 6 of 6 ländlerische Tänze +Title-Opus: WoO 15-6 +Title-Dates: 1802 + +Title-Type: Dance +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: No. 6 of 6 ländlerische Tänze, piano version +Title-Opus: WoO 15-6 +Title-Dates: 1802 + +Title-RAW: 12 Ecossaises for Piano; authorship: fraudulent +Title-Opus: WoO 16 + +Title-Type: Waltz +Title-For: Instrument(s) +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 1 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-1 +Title-Dates: 1819 + +Title-Type: Minuet +Title-For: Instrument(s) +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 2 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-2 +Title-Dates: 1819 + +Title-Type: Waltz +Title-For: Instrument(s) +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 3 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-3 +Title-Dates: 1819 + +Title-Type: Minuet +Title-For: Instrument(s) +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 4 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-4 +Title-Dates: 1819 + +Title-Type: Minuet +Title-For: Instrument(s) +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 5 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-5 +Title-Dates: 1819 + +Title-Type: Ländler +Title-For: Instrument(s) +Title-Key: E flat major +Title-Punct: : +Title-Comment: No. 6 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-6 +Title-Dates: 1819 + +Title-Type: Minuet +Title-For: Instrument(s) +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 7 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-7 +Title-Dates: 1819 + +Title-Type: Ländler +Title-For: Instrument(s) +Title-Key: B flat major +Title-Punct: : +Title-Comment: No. 8 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-8 +Title-Dates: 1819 + +Title-Type: Minuet +Title-For: Instrument(s) +Title-Key: G major +Title-Punct: : +Title-Comment: No. 9 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-9 +Title-Dates: 1819 + +Title-Type: Waltz +Title-For: Instrument(s) +Title-Key: D major +Title-Punct: : +Title-Comment: No. 10 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-10 +Title-Dates: 1819 + +Title-Type: Waltz +Title-For: Instrument(s) +Title-Key: D major +Title-Punct: : +Title-Comment: No. 11 of 11 Mödlinger Tänze for 7 String & Wind Instruments; authorship: probably spurious +Title-Opus: WoO 17-11 +Title-Dates: 1819 + +Title-RAW: March & Trio +Title-For: Wind Band +Title-Key: F major +Title-Name: York’scher (Yorkscher) March für die bohmische Landwehr +Title-Opus: WoO 18 +Title-Dates: 1809--1810 + +Title-RAW: March & Trio +Title-For: Wind Band +Title-Key: F major +Title-Opus: WoO 19 +Title-Dates: 1810 + +Title-RAW: March & Trio +Title-For: Wind Band +Title-Key: C major +Title-Name: Zapfenstreich +Title-Comment: (The Tattoo) +Title-Opus: WoO 20 +Title-Dates: 1809--1822 + +Title-Type: Polonaise +Title-For: Wind Band +Title-Key: D major +Title-Opus: WoO 21 +Title-Dates: 1810 + +Title-Type: Ecossaise +Title-For: Wind Band +Title-Key: D major +Title-Opus: WoO 22 +Title-Dates: 1809--1810 + +Title-Type: Ecossaise +Title-For: Wind Band +Title-Key: G major +Title-Punct: ; +Title-Comment: authorship: reconstruction +Title-Opus: WoO 23 +Title-Dates: 1810 + +Title-Type: March +Title-For: Wind Band +Title-Key: D major +Title-Opus: WoO 24 +Title-Dates: 1816 + +Title-Type: Rondino +Title-For: Oboes, Clarinets, Horns & Bassoons +Title-Key: E flat major +Title-Opus: WoO 25 +Title-Dates: 1792--1793 + +Title-Type: Duo +Title-For: Flutes +Title-Key: G major +Title-Punct: : +Title-Comment: Allegro and Minuet +Title-Opus: WoO 26 +Title-Dates: 1792 + +Title-Type: Duo +Title-No: 1 +Title-For: Clarinet & Bassoon +Title-Key: C major +Title-Punct: ; +Title-Comment: authorship: doubtful +Title-Opus: WoO 27-1 + +Title-Type: Duo +Title-No: 2 +Title-For: Clarinet & Bassoon +Title-Key: F major +Title-Punct: ; +Title-Comment: authorship: doubtful +Title-Opus: WoO 27-2 + +Title-Type: Duo +Title-No: 3 +Title-For: Clarinet & Bassoon +Title-Key: B flat major +Title-Punct: ; +Title-Comment: authorship: doubtful +Title-Opus: WoO 27-3 + +Title-Type: Variations +Title-For: Oboes & English Horn +Title-Key: C major +Title-Punct: : +Title-Comment: on "Là ci darem la mano" from Mozart’s opera +Title-Name: Don Giovanni +Title-Opus: WoO 28 +Title-Dates: 1795 + +Title-Type: March +Title-For: Clarinets, Horns & Bassoons +Title-Key: B flat major +Title-Name: Grenadiermarsch +Title-Opus: WoO 29 +Title-Dates: 1797--1798 + +Title-Type: Equali +Title-For: Trombones +Title-Key: D minor +Title-Opus: WoO 30-1 +Title-Dates: 1812 + +Title-Type: Equali +Title-For: Trombones +Title-Key: D major +Title-Opus: WoO 30-2 +Title-Dates: 1812 + +Title-Type: Equali +Title-For: Trombones +Title-Key: B flat major +Title-Opus: WoO 30-3 +Title-Dates: 1812 + +Title-Type: Fugue +Title-For: Organ +Title-Key: D major +Title-Opus: WoO 31 +Title-Dates: 1783 + +Title-Type: Duo +Title-For: Viola & Cello +Title-Key: E flat major +Title-Name: mit zwei obligaten Augengläsern +Title-Comment: (with 2 obbligato eyeglasses) +Title-Opus: WoO 32 +Title-Dates: 1796--1797 + +Title-RAW: Piece for Musical Clock +Title-Key: F major +Title-Punct: : +Title-Comment: or for flute; Adagio assai; No. 1 of 5 pieces +Title-Opus: WoO 33-1 +Title-Dates: 1799 + +Title-RAW: Piece for Musical Clock +Title-Key: G major +Title-Punct: : +Title-Comment: or for flute; Scherzo-Allegro; No. 2 of 5 pieces +Title-Opus: WoO 33-2 +Title-Dates: 1799--1800 + +Title-RAW: Piece for Musical Clock +Title-Key: G major +Title-Punct: : +Title-Comment: or for flute; Allegro; No. 3 of 5 pieces +Title-Opus: WoO 33-3 +Title-Dates: 1799 + +Title-RAW: Piece for Musical Clock +Title-Key: C major +Title-Punct: : +Title-Comment: or for flute; Allegro non piu molto; No. 4 of 5 pieces +Title-Opus: WoO 33-4 +Title-Dates: 1794 + +Title-RAW: Piece for Musical Clock +Title-Key: C major +Title-Punct: : +Title-Comment: or for flute; Minuet-Allegretto; No. 5 of 5 pieces +Title-Opus: WoO 33-5 +Title-Dates: 1794 + +Title-Type: Duo +Title-For: Violins +Title-Key: A major +Title-Punct: : +Title-Comment: for Alexandre Boucher +Title-Opus: WoO 34 +Title-Dates: 1822 + +Title-Type: Canon +Title-Key: A major +Title-Punct: : +Title-Comment: for 2 violins or for 2 cellos; for Samson de Boer +Title-Opus: WoO 35 +Title-Dates: 1825 + +Title-Type: Piano Quartet +Title-Key: E flat major +Title-Opus: WoO 36-1 +Title-Dates: 1785 + +Title-Type: Piano Quartet +Title-Key: D major +Title-Opus: WoO 36-2 +Title-Dates: 1785 + +Title-Type: Piano Quartet +Title-Key: C major +Title-Opus: WoO 36-3 +Title-Dates: 1785 + +Title-Type: Trio +Title-For: Piano, Flute & Bassoon +Title-Key: G major +Title-Opus: WoO 37 +Title-Dates: 1786 + +Title-Type: Piano Trio +Title-No: 8 +Title-Key: E flat major +Title-Opus: WoO 38 +Title-Dates: 1785--1791 + +Title-Type: Allegretto +Title-For: Piano Trio +Title-Key: B flat major +Title-Opus: WoO 39 +Title-Dates: 1812 + +Title-Count: 12 +Title-Type: Variations +Title-For: Piano & Violin +Title-Key: F major +Title-Punct: : +Title-Comment: on the theme "Se vuol ballare" from Mozart’s opera +Title-Name: The Marriage of Figaro +Title-Opus: WoO 40 +Title-Dates: 1792--1793 + +Title-Type: Rondo +Title-For: Piano & Violin +Title-Key: G major +Title-Opus: WoO 41 +Title-Dates: 1793--1794 + +Title-RAW: 6 German Dances for Piano & Violin: Allemandes in F +Title-Key: D major +Title-Key: F major +Title-Key: A major +Title-Key: D major +Title-Key: G major +Title-Opus: WoO 42 +Title-Dates: 1796 + +Title-Type: Sonatina +Title-For: Mandolin & Harpsichord +Title-Key: C minor +Title-Punct: : +Title-Comment: (WoO 43a) +Title-Opus: WoO 43-1 +Title-Dates: 1796 + +Title-RAW: Adagio +Title-For: Mandolin & Harpsichord +Title-Key: E flat major +Title-Punct: : +Title-Comment: (WoO 43b) +Title-Opus: WoO 43-2 +Title-Dates: 1796 + +Title-Type: Sonatina +Title-For: Mandolin & Harpsichord +Title-Key: C major +Title-Punct: : +Title-Comment: (WoO 44a) +Title-Opus: WoO 44-1 +Title-Dates: 1796 + +Title-RAW: Andante con Variazioni +Title-For: Mandolin & Harpsichord +Title-Key: D major +Title-Punct: : +Title-Comment: (WoO 44b) +Title-Opus: WoO 44-2 +Title-Dates: 1796 + +Title-Count: 12 +Title-Type: Variations +Title-For: Piano & Cello +Title-Key: G major +Title-Punct: : +Title-Comment: on "See the conquering hero comes" fm Handel’s oratorio +Title-Name: Judas Maccabaeus +Title-Opus: WoO 45 +Title-Dates: 1796 + +Title-Count: 7 +Title-Type: Variations +Title-For: Piano & Cello +Title-Key: E flat major +Title-Punct: : +Title-Comment: on the duet "Bei Männern, welche Liebe fühlen" from Mozart’s +Title-Name: Zauberflöte +Title-Opus: WoO 46 +Title-Dates: 1801 + +Title-Type: Piano Sonata +Title-Key: E flat major +Title-Name: Kurfürstensonate +Title-Comment: (Electoral) +Title-No: 1 +Title-Opus: WoO 47-1 +Title-Dates: 1783 + +Title-Type: Piano Sonata +Title-Key: F minor +Title-Name: Kurfürstensonate +Title-Comment: (Electoral) +Title-No: 2 +Title-Opus: WoO 47-2 +Title-Dates: 1783 + +Title-Type: Piano Sonata +Title-Key: D major +Title-Name: Kurfürstensonate +Title-Comment: (Electoral) +Title-No: 3 +Title-Opus: WoO 47-3 +Title-Dates: 1783 + +Title-Type: Rondo +Title-For: Piano +Title-Key: C major +Title-Opus: WoO 48 +Title-Dates: 1783 + +Title-Type: Rondo +Title-For: Piano +Title-Key: A major +Title-Opus: WoO 49 +Title-Dates: 1783 + +Title-RAW: 2 Sonata Movements +Title-For: Piano +Title-Key: F major +Title-Opus: WoO 50 +Title-Dates: 1790--1792 + +Title-RAW: Easy Piano Sonata +Title-Key: C major +Title-Opus: WoO 51 +Title-Dates: 1791--1798 + +Title-Type: Bagatelle +Title-For: Piano +Title-Key: C minor +Title-Punct: : +Title-Comment: presto +Title-Opus: WoO 52 +Title-Dates: 1795--1822 + +Title-Type: Allegretto +Title-For: Piano +Title-Key: C minor +Title-Opus: WoO 53 +Title-Dates: 1796--1797 + +Title-Type: Piece +Title-For: Piano +Title-Key: C major +Title-Name: Lustig - traurig +Title-Comment: (Merry - Sad) in C & c +Title-Opus: WoO 54 +Title-Dates: 1802 + +Title-Type: Prelude +Title-For: Piano +Title-Key: F minor +Title-Opus: WoO 55 +Title-Dates: 1803 + +Title-Type: Bagatelle +Title-For: Piano +Title-Key: C major +Title-Punct: : +Title-Comment: Allegretto +Title-Opus: WoO 56 +Title-Dates: 1803--1822 + +Title-Type: Andante +Title-For: Piano +Title-Key: F major +Title-Name: Andante favori +Title-Opus: WoO 57 +Title-Dates: 1803--1804 + +Title-Type: Cadenza +Title-For: Piano +Title-Key: D minor +Title-Punct: : +Title-Comment: for Mozart’s piano concerto in d minor K 466 1st mvmt +Title-Opus: WoO 58-1 +Title-Dates: 1809 + +Title-Type: Cadenza +Title-For: Piano & Orchestra +Title-Key: D minor +Title-Punct: : +Title-Comment: for Mozart’s piano concerto in d minor K 466 3rd mvmt +Title-Opus: WoO 58-2 +Title-Dates: 1809 + +Title-Type: Bagatelle +Title-For: Piano +Title-Key: A minor +Title-Name: Für Elise +Title-Opus: WoO 59 +Title-Dates: 1808 + +Title-Type: Bagatelle +Title-For: Piano +Title-Key: B flat major +Title-Name: Zeimlich lebhaft +Title-Comment: (A little lively) +Title-Opus: WoO 60 +Title-Dates: 1818 + +Title-Type: Allegretto +Title-For: Piano +Title-Key: B minor +Title-Opus: WoO 61 +Title-Dates: 1821 + +Title-Type: Bagatelle +Title-For: Piano +Title-Key: G minor +Title-Punct: : +Title-Comment: called +Title-Opus: WoO 61a +Title-Opus: WoO 61-1 +Title-Dates: 1825 + +Title-Type: String Quintet +Title-Key: C major +Title-Name: Letzter musikalischer Gedanke +Title-Comment: (last musical thought); authorship: unfinished at death +Title-Opus: WoO 62 +Title-Dates: 1826 + +Title-Count: 9 +Title-Type: Variations +Title-For: Piano +Title-Key: C minor +Title-Punct: : +Title-Comment: on a march by Ernst Christoph Dressler +Title-Opus: WoO 63 +Title-Dates: 1782 + +Title-Count: 6 +Title-Type: Variations +Title-For: Piano or Harp +Title-Key: F major +Title-Punct: : +Title-Comment: on a Swiss air +Title-Opus: WoO 64 +Title-Dates: 1790--1792 + +Title-Count: 24 +Title-Type: Variations +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: on Vincenzo Righini’s arietta +Title-Name: Venni Amore +Title-Opus: WoO 65 +Title-Dates: 1790--1791 + +Title-Count: 13 +Title-Type: Variations +Title-For: Piano +Title-Key: A major +Title-Punct: : +Title-Comment: on Dittersdorf’s arietta +Title-Name: Es war einmal ein alter Mann +Title-Related-How: from +Title-Related-Name: Das rote Käppchen +Title-Opus: WoO 66 +Title-Dates: 1792 + +Title-Count: 8 +Title-Type: Variations +Title-For: Piano Four Hands +Title-Key: C major +Title-Punct: : +Title-Comment: on a theme by Count Ferdinand von Waldstein +Title-Opus: WoO 67 +Title-Dates: 1792 + +Title-Count: 12 +Title-Type: Variations +Title-For: Piano +Title-Key: C major +Title-Punct: : +Title-Comment: on the "Menuett à la Viganò" from Jakob Haibel’s ballet +Title-Name: Le nozze disturbate +Title-Opus: WoO 68 +Title-Dates: 1795 + +Title-Count: 9 +Title-Type: Variations +Title-For: Piano +Title-Key: A major +Title-Punct: : +Title-Comment: on Giovanni Paisello’s air +Title-Name: Quant’ è più bello +Title-Related-How: from +Title-Related-Name: La Molinara +Title-Opus: WoO 69 +Title-Dates: 1795 + +Title-Count: 6 +Title-Type: Variations +Title-For: Piano +Title-Key: G major +Title-Punct: : +Title-Comment: on the duet "Nel cor più non mi sento" from Giovanni Paisello’s opera +Title-Name: La Molinara +Title-Opus: WoO 70 +Title-Dates: 1795 + +Title-Count: 12 +Title-Type: Variations +Title-For: Piano +Title-Key: A major +Title-Punct: : +Title-Comment: on a Russian dance from Paul Wranitsky’s ballet +Title-Name: Das Waldmädchen +Title-Opus: WoO 71 +Title-Dates: 1796--1797 + +Title-Count: 8 +Title-Type: Variations +Title-For: Piano +Title-Key: C major +Title-Punct: : +Title-Comment: on the Romance "Une fièvre brûlante" from Grétry’s opera +Title-Name: Richard Cœur de Lion +Title-Opus: WoO 72 +Title-Dates: 1795--1798 + +Title-Count: 10 +Title-Type: Variations +Title-For: Piano +Title-Key: B major +Title-Punct: : +Title-Comment: on Salieri’s duet +Title-Name: La stessa la stessissima +Title-Related-How: from +Title-Related-Name: Falstaff +Title-Opus: WoO 73 +Title-Dates: 1799 + +Title-Type: Song +Title-Name: Ich denke dein +Title-Alternative-Name: Ich denke dein, wenn mir der Sonne Schimmer +Title-Opus: WoO 74 +Title-Dates: 1799--1803 + +Title-Count: 6 +Title-Type: Variations +Title-For: Piano Four Hands +Title-Key: D major +Title-Punct: : +Title-Related-How: on +Title-Related-Name: Ich denke dein +Title-Opus: WoO 74 +Title-Dates: 1799--1803 + +Title-Count: 7 +Title-Type: Variations +Title-For: Piano +Title-Key: F major +Title-Punct: : +Title-Comment: on the Quartet "Kind, willst du ruhig schlafen?" from Peter Winter’s opera +Title-Name: Das unterbrochene Opferfest +Title-Opus: WoO 75 +Title-Dates: 1792--1799 + +Title-Count: 8 +Title-Type: Variations +Title-For: Piano +Title-Key: F major +Title-Punct: : +Title-Comment: on the Trio "Tändeln und Scherzen" from Franz Xaver Süssmayr’s opera +Title-Name: Soliman II +Title-Opus: WoO 76 +Title-Dates: 1799 + +Title-Count: 6 +Title-Type: Easy Variations +Title-For: Piano +Title-Key: G major +Title-Punct: : +Title-Comment: on an original theme +Title-Opus: WoO 77 +Title-Dates: 1800 + +Title-Count: 7 +Title-Type: Variations +Title-For: Piano +Title-Key: C major +Title-Punct: : +Title-Comment: on the English folk song +Title-Name: God Save the King +Title-Opus: WoO 78 +Title-Dates: 1802--1803 + +Title-Count: 5 +Title-Type: Variations +Title-For: Piano +Title-Key: D major +Title-Punct: : +Title-Comment: on the English song "Rule Britannia" from Arne’s +Title-Name: Alfred +Title-Opus: WoO 79 +Title-Dates: 1803 + +Title-Count: 32 +Title-Type: Variations +Title-For: Piano +Title-Key: C minor +Title-Punct: : +Title-Comment: on an original theme +Title-Opus: WoO 80 +Title-Dates: 1806 + +Title-Type: Allemande +Title-For: Piano +Title-Key: A major +Title-Opus: WoO 81 +Title-Dates: 1793--1822 + +Title-Type: Minuet +Title-For: Piano +Title-Key: E flat major +Title-Opus: WoO 82 +Title-Dates: 1803 + +Title-Count: 6 +Title-Type: Ecossaises +Title-For: Piano +Title-Key: E flat major +Title-Opus: WoO 83 +Title-Dates: 1806 + +Title-Type: Waltz +Title-For: Piano +Title-Key: E flat major +Title-Opus: WoO 84 +Title-Dates: 1824 + +Title-Type: Waltz +Title-For: Piano +Title-Key: D major +Title-Opus: WoO 85 +Title-Dates: 1825 + +Title-Type: Ecossaise +Title-For: Piano +Title-Key: E flat major +Title-Opus: WoO 86 +Title-Dates: 1825 + +Title-RAW: Cantata "Trauerkantate auf den Tod Joseph II" (on the Death of Joseph II) +Title-Opus: WoO 87 +Title-Dates: 1790 + +Title-RAW: Cantata "Kantate auf die Erhebung Leopold II zur Kaiserwürde" (on the Elevation of Leopold II to the Imperial Dignity) +Title-Opus: WoO 88 +Title-Dates: 1790 + +Title-Type: Aria +Title-Key: F major +Title-Name: Prüfung des Küssens +Title-Punct: ; +Title-For: bass & orchestra +Title-Name: Meine weise Mutter spricht +Title-Opus: WoO 89 +Title-Dates: 1790--1792 + +Title-Type: Aria +Title-Key: D major +Title-Name: Mit Mädeln sich vertragen +Title-Punct: ; +Title-For: bass & orchestra +Title-Opus: WoO 90 +Title-Dates: 1790--1792 + +Title-Type: Aria +Title-Key: F major +Title-Name: O welch ein Leben! +Title-Punct: ; +Title-Comment: for tenor & orchestra for Umlauf’s Singspiel "Die schöne Schusterin" (The Beautiful Shoemaker’s Wife) +Title-Opus: WoO 91-1 +Title-Dates: 1795 + +Title-Type: Aria +Title-Key: B flat major +Title-Name: Soll ein Schuh nicht drücken? +Title-Punct: ; +Title-Comment: for soprano & orchestra for Umlauf’s Singspiel "Die schöne Schusterin" (The Beautiful Shoemaker’s Wife) +Title-Opus: WoO 91-2 +Title-Dates: 1795 + +Title-RAW: Scene & Aria +Title-Key: A major +Title-Name: Primo amore +Title-Punct: ; +Title-Comment: for soprano w/ orchestra +Title-Opus: WoO 92 +Title-Dates: 1791--1792 + +Title-RAW: Scene & Aria "No, non turbarti"; for soprano w/ string orchestra (WoO 92a) +Title-Opus: WoO 92-1 +Title-Dates: 1802 + +Title-RAW: Duo for Solo Voice(s) & Orchestra "Ne’ giorni tuoi felici"; for soprano & tenor w/ orchestra +Title-Opus: WoO 93 +Title-Dates: 1802 + +Title-RAW: Music for a drama +Title-For: Orchestra, Chorus & Solo Voice(s) +Title-Key: B flat major +Title-Name: Germania +Title-Punct: , +Title-Comment: finale for Trietschke’s Singspiel "Die gute Nachricht" (The Good News) +Title-Opus: WoO 94 +Title-Dates: 1814 + +Title-Type: Chorus +Title-For: Orchestra & Chorus +Title-Key: A major +Title-Name: Chor auf die verbündeten Fürsten +Title-Comment: (Chorus for the Allied Princes) +Title-Name: Ihr weisen Gründer +Title-Opus: WoO 95 +Title-Dates: 1814 + +Title-RAW: Music for a drama +Title-For: Orchestra, Chorus & Solo Voice(s) +Title-Name: Leonore Prohaska +Title-Opus: WoO 96 +Title-Dates: 1815 + +Title-Type: Chorus +Title-For: Orchestra, Chorus & Solo Voice(s) +Title-Key: D major +Title-Name: Es ist vollbracht +Title-Comment: (It is fulfilled), final chorus from Treitschke’s Singspiel "Die Ehrenpforten" (The Triumphal Arches) +Title-Opus: WoO 97 +Title-Dates: 1815 + +Title-RAW: Chorus for Orchestra, Chorus & Solo Voice(s) "Wo sich die Pulse jugendlich jagen" chorus with soprano solo for the festive play +Title-Name: Die Weihe des Hauses +Title-Opus: WoO 98 +Title-Dates: 1822 + +Title-Type: Duet +Title-Name: Bei labbri, che Amore +Title-Opus: WoO 99-1 +Title-Dates: 1801--1803 + +Title-Type: Trio +Title-For: Solo Voice(s) +Title-Name: Chi mai di questo core +Title-Opus: WoO 99-2 +Title-Dates: 1801--1803 + +Title-RAW: Duet "Fra tutte le pene" (WoO 99, No. 3a) 1 of 3 settings +Title-Opus: WoO 99-3.1 +Title-Dates: 1801--1803 + +Title-RAW: Trio for Solo Voice(s) "Fra tutte le pene" (WoO 99, No. 3b); 2 of 3 settings +Title-Opus: WoO 99-3.2 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Fra tutte le pene" (WoO 99, No. 3c) 3 of 3 settings +Title-Opus: WoO 99-3.3 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Già la notte s’avvicina" (WoO 99, No. 4a) 1 of 2 settings +Title-Opus: WoO 99-4.1 +Title-Dates: 1801--1803 + +Title-RAW: Trio for Solo Voice(s) "Già la notte s’avvicina" (WoO 99, No. 4b) 2 of 2 settings +Title-Opus: WoO 99-4.2 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Giura il nocchier" (WoO 99, No. 5a) 1 of 2 settings +Title-Opus: WoO 99-5.1 +Title-Dates: 1801--1803 + +Title-RAW: Trio for Solo Voice(s) "Giura il nocchier" (WoO 99, No. 5b) 2 of 2 settings +Title-Opus: WoO 99-5.2 +Title-Dates: 1801--1803 + +Title-Type: Trio +Title-For: Solo Voice(s) +Title-Name: Ma tu tremi +Title-Opus: WoO 99-6 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Nei campi e nelle selve" (WoO 99, No. 7a) 1 of 2 settings +Title-Opus: WoO 99-7.1 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Nei campi e nelle selve" (WoO 99, No. 7b) 2 of 2 settings +Title-Opus: WoO 99-7.2 +Title-Dates: 1801--1803 + +Title-Type: Song +Title-Key: G major +Title-Name: O care selve +Title-Punct: , +Title-Comment: with unison chorus +Title-Opus: WoO 99-8 +Title-Dates: 1794--1795 + +Title-Type: Trio +Title-For: Solo Voice(s) +Title-Name: Per te d’amico aprile +Title-Opus: WoO 99-9 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Quella cetra ah pur tu sei" (WoO 99, No. 10a) 1 of 3 settings +Title-Opus: WoO 99-10.1 +Title-Dates: 1801--1803 + +Title-RAW: Trio for Solo Voice(s) "Quella cetra ah pur tu sei" (WoO 99, No. 10b) 2 of 3 settings +Title-Opus: WoO 99-10.2 +Title-Dates: 1801--1803 + +Title-RAW: Quartet for Solo Voice(s) "Quella cetra ah pur tu sei" (WoO 99, No. 10c) 3 of 3 settings +Title-Opus: WoO 99-10.3 +Title-Dates: 1801--1803 + +Title-Type: Duet +Title-Name: Scrivo in te +Title-Opus: WoO 99-11 +Title-Dates: 1801--1803 + +Title-Type: Quartet +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Silvio, amante disperato +Title-Opus: WoO 99-12 +Title-Dates: 1801--1802 + +Title-Type: Joke +Title-Key: G major +Title-Name: Lob auf den Dicken +Title-Name: Schuppanzigh ist ein Lump +Title-Opus: WoO 100 +Title-Dates: 1801 + +Title-Type: Joke +Title-Key: E flat major +Title-Name: Graf, liebster Graf, liebstes Schaf +Title-Opus: WoO 101 +Title-Dates: 1802 + +Title-RAW: Song "Abschiedsgesang" (Song of Farewell) +Title-For: tenor & 2 basses +Title-Name: Die Stunde schlägt +Title-Opus: WoO 102 +Title-Dates: 1814 + +Title-Type: Cantata +Title-Key: B flat major +Title-Name: Un lieto brindisi: Cantata campestre +Title-Comment: for 4 voices & piano acc +Title-Name: Johannisfeier begehn wir heute +Title-Opus: WoO 103 +Title-Dates: 1814 + +Title-RAW: Song "Gesang der Mönche" (Song of the Monks) from Schiller’s +Title-Name: William Tell +Title-Alternative-Name: Rasch tritt der Tod den Menchen an +Title-Opus: WoO 104 +Title-Dates: 1817 + +Title-Type: Song +Title-Key: C major +Title-Name: Hochzeitslied +Title-For: bass, chorus & piano +Title-Name: Auf Freunde, singt dem Gott der Ehen! +Title-Opus: WoO 105 +Title-Dates: 1819 + +Title-Type: Song +Title-Key: A major +Title-Name: Hochzeitslied +Title-Comment: unison version of Hess 124 +Title-Opus: WoO 105 +Title-Dates: 1819 + +Title-Type: Cantata +Title-Name: Lobkowitz-Kantate +Title-For: soprano, chorus & piano +Title-Opus: WoO 106 +Title-Dates: 1823 + +Title-Type: Song +Title-Name: Schilderung eines Mädchens +Title-Alternative-Name: Schildern, willst du Freund, soll ich dir Elisen? +Title-Opus: WoO 107 +Title-Dates: 1782 + +Title-Type: Song +Title-Key: A major +Title-Name: An einen Säugling +Title-Alternative-Name: Noch weisst du nicht, wes Kind du bist +Title-Opus: WoO 108 +Title-Dates: 1783 + +Title-Type: Song +Title-Key: C major +Title-Name: Trinklied (beim Abschied zu singen) +Title-Alternative-Name: Erhebt das Glas mit froher Hand +Title-Opus: WoO 109 +Title-Dates: 1791--1792 + +Title-Type: Song +Title-Key: F minor +Title-Name: Elegie auf den Tod eines Pudels +Title-Alternative-Name: Stirb immerhin, es welken ja so viele der Freuden +Title-Opus: WoO 110 +Title-Dates: 1790 + +Title-Type: Song +Title-Key: G major +Title-Name: Punschlied +Title-Alternative-Name: Wer nicht, wenn warm von Hand zu Hand +Title-Opus: WoO 111 +Title-Dates: 1791--1792 + +Title-Type: Song +Title-Key: G major +Title-Name: An Laura +Title-Alternative-Name: Freud’ umblühe dich auf allen Wegen +Title-Opus: WoO 112 +Title-Dates: 1792 + +Title-Type: Song +Title-Key: E major +Title-Name: Klage +Title-Alternative-Name: Dein Silver schien durch Eichengrün +Title-Opus: WoO 113 +Title-Dates: 1790 + +Title-Type: Song +Title-Key: E major +Title-Name: Ein Selbstgespräch +Title-Alternative-Name: Ich, der mit flatterndem Sinn +Title-Opus: WoO 114 +Title-Dates: 1793 + +Title-Type: Song +Title-Key: D major +Title-Name: An Minna +Title-Alternative-Name: Nur bei dir, an deinem Herzen +Title-Opus: WoO 115 +Title-Dates: 1792 + +Title-Type: Song +Title-Key: C minor +Title-Name: Que le temps me dure +Title-Comment: 1st version +Title-Opus: WoO 116 +Title-Dates: 1793 + +Title-Type: Song +Title-Key: C major +Title-Name: Que le temps me dure +Title-Comment: 2nd version +Title-Opus: WoO 116 +Title-Dates: 1793 + +Title-Type: Song +Title-Key: C major +Title-Name: Der freie Mann +Title-Punct: ; +Title-Comment: "Wer ist ein freier Mann?"; with unison chorus +Title-Opus: WoO 117 +Title-Dates: 1792--1794 + +Title-RAW: Song "Seufzer eines Ungeliebten"; "Hast du nicht Liebe zugemessen", and +Title-Name: Gegenliebe +Title-Alternative-Name: Wüsst’ ich, wüsst’ ich +Title-Opus: WoO 118 +Title-Dates: 1794--1795 + +Title-Type: Song +Title-Key: G major +Title-Name: O care selve +Title-Punct: , +Title-Comment: with unison chorus +Title-Opus: WoO 119 +Title-Dates: 1794--1795 + +Title-Type: Song +Title-Key: F major +Title-Name: Man strebt, die Flamme zu verhehlen +Title-Opus: WoO 120 +Title-Dates: 1800--1802 + +Title-Type: Song +Title-Key: G major +Title-Name: Abschiedsgesang an Wiens Bürger +Title-Alternative-Name: Keine Klage soll erschallen +Title-Opus: WoO 121 +Title-Dates: 1796 + +Title-Type: Song +Title-Key: C major +Title-Name: Kriegslied der Osterreicher +Title-Punct: ; +Title-Comment: "Ein grosses deutches Volk sind wir" (with unison chorus) +Title-Opus: WoO 122 +Title-Dates: 1797 + +Title-Type: Song +Title-Key: G major +Title-Name: Zärtliche Liebe +Title-Comment: (Tender Love) +Title-Alternative-Name: Ich liebe dich, so wie du mich +Title-Opus: WoO 123 +Title-Dates: 1795 + +Title-Type: Song +Title-Key: A major +Title-Name: La partenza +Title-Comment: (Der Abschied) +Title-Alternative-Name: Ecco quel fiero istante! +Title-Opus: WoO 124 +Title-Dates: 1795 + +Title-Type: Song +Title-Key: E flat major +Title-Name: La tiranna +Title-Punct: , +Title-Comment: Canzonetta +Title-Alternative-Name: Ah grief to think! +Title-Opus: WoO 125 +Title-Dates: 1798--1799 + +Title-Type: Song +Title-Key: E major +Title-Name: Opferlied +Title-Comment: (Sacrificial Song) +Title-Alternative-Name: Die Flamme lodert +Title-Opus: WoO 126 +Title-Dates: 1794--1795 + +Title-Type: Song +Title-Key: C major +Title-Name: Neue Liebe, neues Leben +Title-Comment: (New love, new life); "Herz, mein Herz, was soll das geben?" (1st setting) +Title-Opus: WoO 127 +Title-Dates: 1798--1799 + +Title-Type: Song +Title-Key: G major +Title-Name: Plaisir d’aimer +Title-Name: Plaisir d’aimer besoin d’une âme tendre +Title-Opus: WoO 128 +Title-Dates: 1798--1799 + +Title-Type: Song +Title-Key: F major +Title-Name: Der Wachtelschlag +Title-Comment: (The Quail’s Call) +Title-Name: Ach, wie schalt’s dorten so lieblich hervor +Title-Opus: WoO 129 +Title-Dates: 1803 + +Title-Type: Song +Title-Key: C minor +Title-Name: Gedenke mein +Title-Alternative-Name: Gedenke mein, ich denke dein +Title-Opus: WoO 130 +Title-Dates: 1804--1820 + +Title-RAW: Sketch +Title-For: Solo Voice(s) & Instrument(s) +Title-Key: D minor +Title-Name: Erlkönig +Title-Comment: "Wer reitet so spat durch Nacht und Wind?"; authorship: unfinished +Title-Opus: WoO 131 +Title-Dates: 1794--1796 + +Title-Type: Song +Title-Key: E flat major +Title-Name: Als die Geliebte sich trennen wollte +Title-Alternative-Name: Der Hoffnung letzter Schimmer sinkt dahin +Title-Opus: WoO 132 +Title-Dates: 1806 + +Title-Type: Song +Title-Key: A flat major +Title-Name: In questa tomba oscura +Title-Punct: , +Title-Comment: Arietta (In this dark Tomb) +Title-Opus: WoO 133 +Title-Dates: 1806--1807 + +Title-Type: Song +Title-Key: G minor +Title-Name: Sehnsucht +Title-Punct: ; +Title-Comment: "Nur wer die Sehnsucht kennt" (1st setting) +Title-Opus: WoO 134-1 +Title-Dates: 1807--1808 + +Title-Type: Song +Title-Key: G minor +Title-Name: Sehnsucht +Title-Punct: ; +Title-Comment: "Nur wer die Sehnsucht kennt" (2nd setting) +Title-Opus: WoO 134-2 +Title-Dates: 1807--1808 + +Title-Type: Song +Title-Key: E flat major +Title-Name: Sehnsucht +Title-Punct: ; +Title-Comment: "Nur wer die Sehnsucht kennt" (3rd setting) +Title-Opus: WoO 134-3 +Title-Dates: 1807--1808 + +Title-Type: Song +Title-Key: G minor +Title-Name: Sehnsucht +Title-Punct: ; +Title-Comment: "Nur wer die Sehnsucht kennt" (4th setting) +Title-Opus: WoO 134-4 +Title-Dates: 1807--1808 + +Title-Type: Song +Title-Key: C minor +Title-Name: Die laute Klage +Title-Alternative-Name: Turteltaube, du klagtest so laut +Title-Opus: WoO 135 +Title-Dates: 1814--1815 + +Title-Type: Song +Title-Key: D major +Title-Name: Andenken +Title-Comment: (Memories) +Title-Alternative-Name: Ich denke dein +Title-Opus: WoO 136 +Title-Dates: 1808 + +Title-Type: Song +Title-Key: B flat major +Title-Name: Gesang aus der Ferne +Title-Comment: (Song from far away); "Als mir noch die Träne" (2nd setting) +Title-Opus: WoO 137 +Title-Dates: 1809 + +Title-Type: Song +Title-Key: B flat major +Title-Name: Der Jüngling in der Fremde +Title-Alternative-Name: Der Früling entblühet dem Schoß der Natur +Title-Opus: WoO 138 +Title-Dates: 1809 + +Title-Type: Song +Title-Key: D major +Title-Name: Der Liebende +Title-Alternative-Name: Welch ein wunderbares Leben +Title-Opus: WoO 139 +Title-Dates: 1809 + +Title-RAW: Song "An die Geliebte"; "O daß ich dir vom stillen Auge" (1st setting) +Title-Opus: WoO 140-1 +Title-Dates: 1811 + +Title-RAW: Song "An die Geliebte"; "O daß ich dir vom stillen Auge" (2nd setting) +Title-Opus: WoO 140-2 +Title-Dates: 1814 + +Title-Type: Song +Title-Key: C major +Title-Name: Der Gesang der Nachtigall +Title-Alternative-Name: Höre, die Nachtigall singt +Title-Opus: WoO 141 +Title-Dates: 1813 + +Title-Type: Song +Title-Key: E minor +Title-Name: Der Bardengeist +Title-Alternative-Name: Dort auf dem hohen Felsen sang +Title-Opus: WoO 142 +Title-Dates: 1813 + +Title-Type: Song +Title-Key: E flat major +Title-Name: Des Kriegers Abschied +Title-Alternative-Name: Ich zieh’ ins Feld von Lieb’entbrannt +Title-Opus: WoO 143 +Title-Dates: 1814 + +Title-Type: Song +Title-Key: E flat major +Title-Name: Merkenstein +Title-Comment: (1st setting) +Title-Opus: WoO 144 +Title-Dates: 1814 + +Title-Type: Song +Title-Key: G major +Title-Name: Das Geheimnis (Liebe and Wahrheit) +Title-Alternative-Name: Wo blüht das Blümchen, das nie verblüht? +Title-Opus: WoO 145 +Title-Dates: 1815 + +Title-Type: Song +Title-Key: E major +Title-Name: Sehnsucht +Title-Comment: (Longing) +Title-Alternative-Name: Die stille Nacht umdunkelt +Title-Opus: WoO 146 +Title-Dates: 1816 + +Title-Type: Song +Title-Key: A major +Title-Name: Ruf vom Berge +Title-Alternative-Name: Wenn ich ein Vöglein wär +Title-Opus: WoO 147 +Title-Dates: 1816 + +Title-Type: Song +Title-Key: F major +Title-Name: So oder so +Title-Alternative-Name: Nord oder Süd! +Title-Opus: WoO 148 +Title-Dates: 1817 + +Title-Type: Song +Title-Key: D major +Title-Name: Resignation +Title-Alternative-Name: Lisch aus, mein Licht! +Title-Opus: WoO 149 +Title-Dates: 1817 + +Title-Type: Song +Title-Key: E major +Title-Name: Abendlied unter’m gestirnten Himmel +Title-Comment: (Evening Song beneath the Starry Sky) +Title-Alternative-Name: Wenn die Sonne nieder sinket +Title-Opus: WoO 150 +Title-Dates: 1820 + +Title-Type: Song +Title-Key: G major +Title-Name: Der edle Mensch sei hülfreich und gut +Title-Opus: WoO 151 +Title-Dates: 1823 + +Title-RAW: Folksong Setting "The Return to Ulster", "Once again, but how chang’d"; No. 1 of 25 Irish songs +Title-Opus: WoO 152-1 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Sweet Power of Song!"; No. 2 of 25 Irish songs +Title-Opus: WoO 152-2 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Once more I hail thee"; No. 3 of 25 Irish songs +Title-Opus: WoO 152-3 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The morning air plays on my face"; No. 4 of 25 Irish songs +Title-Opus: WoO 152-4 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "On the massacre of Glencoe", "O! tell me, harper"; No. 5 of 25 Irish songs +Title-Opus: WoO 152-5 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "What shall I do to shew how much I love her?"; No. 6 of 25 Irish songs +Title-Opus: WoO 152-6 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "His boat comes on the sunny tide"; No. 7 of 25 Irish songs +Title-Opus: WoO 152-7 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Come draw we round a cheerful ring"; No. 8 of 25 Irish songs +Title-Opus: WoO 152-8 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Soldier’s Dream", "Our bugles sung truce"; No. 9 of 25 Irish songs +Title-Opus: WoO 152-9 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Deserter" (The evening previous to his execution), "If sadly thinking and spirits sinking"; No. 10 of 25 Irish songs +Title-Opus: WoO 152-10 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "Thou emblem of faith" (Upon returning a ring); No. 11 of 25 Irish songs +Title-Opus: WoO 152-11 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "English Bulls, or The Irishman in London", "Och! have you not heard, Pat"; No. 12 of 25 Irish songs +Title-Opus: WoO 152-12 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Musing on the roaring ocean"; No. 13 of 25 Irish songs +Title-Opus: WoO 152-13 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "Dermot and Shelah", "O who sits so sadly"; No. 14 of 25 Irish songs +Title-Opus: WoO 152-14 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Let brainspinning swains"; No. 15 of 25 Irish songs +Title-Opus: WoO 152-15 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Hide not thy anguish"; No. 16 of 25 Irish songs +Title-Opus: WoO 152-16 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "In vain to this desert my fate I deplore"; No. 17 of 25 Irish songs +Title-Opus: WoO 152-17 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "They bid me slight my Dermot dear"; No. 18 of 25 Irish songs +Title-Opus: WoO 152-18 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Wife, Children and Friends", "When the blackletter’d list to the gods"; No. 19 of 25 Irish songs +Title-Opus: WoO 152-19 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "Farewell bliss and farewell Nancy"; No. 20 of 25 Irish songs +Title-Opus: WoO 152-20 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Morning a cruel turmoiler is"; No. 21 of 25 Irish songs +Title-Opus: WoO 152-21 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "From Garyone, my happy home", air: "Garyone"; No. 22 of 25 Irish songs +Title-Opus: WoO 152-22 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "A wand’ring gypsey, Sirs, am I"; No. 23 of 25 Irish songs +Title-Opus: WoO 152-23 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Traugh Welcome", "Shall a son of O’Donnell be cheerless and cold"; No. 24 of 25 Irish songs +Title-Opus: WoO 152-24 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "O harp of Erin", "O harp of Erin thou art now laid low"; air: "I once had a true love"; No. 25 of 25 Irish songs +Title-Opus: WoO 152-25 +Title-Dates: 1812 + +Title-RAW: Folksong Setting "When eve’s last rays in twilight die"; No. 1 of 20 Irish songs +Title-Opus: WoO 153-1 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "No. riches from his scanty store"; No. 2 of 20 Irish songs +Title-Opus: WoO 153-2 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The British Light Dragoons, or The Plain of Badajos", "’Twas a Marechal of France"; No. 3 of 20 Irish songs +Title-Opus: WoO 153-3 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Since greybeards inform us that youth will decay"; No. 4 of 20 Irish songs +Title-Opus: WoO 153-4 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "I dream’d I lay where flow’rs were springing"; No. 5 of 20 Irish songs +Title-Opus: WoO 153-5 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Sad and luckless was the season"; No. 6 of 20 Irish songs +Title-Opus: WoO 153-6 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "O soothe me, my lyre"; No. 7 of 20 Irish songs +Title-Opus: WoO 153-7 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Norah of Balamagairy", "Farewell mirth and hilarity"; No. 8 of 20 Irish songs +Title-Opus: WoO 153-8 +Title-Dates: 1812--1813 + +Title-RAW: Folksong Setting "The kiss, dear maid, thy lip has left"; No. 9 of 20 Irish songs +Title-Opus: WoO 153-9 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Oh, thou hapless soldier"; No. 10 of 20 Irish songs +Title-Opus: WoO 153-10 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "When far from the home"; No. 11 of 20 Irish songs +Title-Opus: WoO 153-11 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "I’ll praise the saints with early song"; No. 12 of 20 Irish songs +Title-Opus: WoO 153-12 +Title-Dates: 1813 + +Title-RAW: Folksong Setting: ‘"Tis sunshine at last"; No. 13 of 20 Irish songs +Title-Opus: WoO 153-13 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Paddy O’Rafferty"; No. 14 of 20 Irish songs +Title-Opus: WoO 153-14 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "‘Tis but in vain, for nothing thrives"; No. 15 of 20 Irish songs +Title-Opus: WoO 153-15 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "O might I but my Patrick love!" (English); No. 16 of 20 Irish songs +Title-Opus: WoO 153-16 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Come, Darby dear! easy, be easy"; No. 17 of 20 Irish songs +Title-Opus: WoO 153-17 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "No. more, my Mary, I sigh for splendour"; No. 18 of 20 Irish songs +Title-Opus: WoO 153-18 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Judy, lovely, matchless creature"; No. 19 of 20 Irish songs +Title-Opus: WoO 153-19 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Thy ship must sail, my Henry dear"; No. 20 of 20 Irish songs +Title-Opus: WoO 153-20 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The Elfin Fairies", "We fairy elves in secret dells", air: "Planxty Kelly"; No. 1 of 12 Irish songs +Title-Opus: WoO 154-1 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "O harp of Erin", "O harp of Erin thou art now laid low", air: "I once had a true love"; No. 2 of 12 Irish songs +Title-Opus: WoO 154-2 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The Farewell Song", "O Erin", air: "The old woman"; No. 3 of 12 Irish songs +Title-Opus: WoO 154-3 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The pulse of a Irishman", air: "St Patrick’s Day"; No. 4 of 12 Irish songs +Title-Opus: WoO 154-4 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "O who, my dear Dermot", air: "Crooghan a Venee"; No. 5 of 12 Irish songs +Title-Opus: WoO 154-5 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Put round the bright wine", air: "Chiling O’Guiry"; No. 6 of 12 Irish songs +Title-Opus: WoO 154-6 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "From Garyone, my happy home", air: "Garyone"; No. 7 of 12 Irish songs +Title-Opus: WoO 154-7 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Save me from the grave and wise", air: "Nora Creina"; No. 8 of 12 Irish songs +Title-Opus: WoO 154-8 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "O would I were but that sweet linnet!", air: "The pretty girl milking the cows"; No. 9 of 12 Irish songs +Title-Opus: WoO 154-9 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The hero may perish", air: "The fox’s sleep"; No. 10 of 12 Irish songs +Title-Opus: WoO 154-10 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The Soldier in a Foreign Land", "The piper who sat on his low mossy seat", air: "The Brown Maid"; No. 11 of 12 Irish songs +Title-Opus: WoO 154-11 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "He promis’d me at parting", air: "Killeavy"; No. 12 of 12 Irish songs +Title-Opus: WoO 154-12 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Chase of the Wolf" or "Sion, the Son of Evan", "Hear the shouts of Evan’s son"; No. 1 of 26 Welsh songs +Title-Opus: WoO 155-1 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Monks of Bangor’s March", "When the heathen trumpet’s clang"; No. 2 of 26 Welsh songs +Title-Opus: WoO 155-2 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Cottage Maid", "I envy not the splendour fine"; No. 3 of 26 Welsh songs +Title-Opus: WoO 155-3 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Love without Hope", "Her features speak the warmest heart"; No. 4 of 26 Welsh songs +Title-Opus: WoO 155-4 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Golden Robe", "A golden robe my Love shall wear"; No. 5 of 26 Welsh songs +Title-Opus: WoO 155-5 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Fair Maid of Mona", "How, my love, could hapless doubts o’ertake thee"; No. 6 of 26 Welsh songs +Title-Opus: WoO 155-6 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "O let the night my blushes hide"; No. 7 of 26 Welsh songs +Title-Opus: WoO 155-7 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Farewell, thou noisy town"; No. 8 of 26 Welsh songs +Title-Opus: WoO 155-8 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "To the Aeolian Harp", "Harp of the winds"; No. 9 of 26 Welsh songs +Title-Opus: WoO 155-9 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Ned Pugh’s Farewell", "To leave my dear girl, my country, and friends"; No. 10 of 26 Welsh songs +Title-Opus: WoO 155-10 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Merch Megan, or Peggy’s Daughter"1 "In the white cot where Peggy dwells"; No. 11 of 26 Welsh songs +Title-Opus: WoO 155-11 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Waken, lords and ladies gay"; No. 12 of 26 Welsh songs +Title-Opus: WoO 155-12 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Helpless Woman", "How cruel are the parents"; No. 13 of 26 Welsh songs +Title-Opus: WoO 155-13 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Dream", "Last night worn with anguish"; No. 14 of 26 Welsh songs +Title-Opus: WoO 155-14 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "When mortals all to rest retire" (English); No. 15 of 26 Welsh songs +Title-Opus: WoO 155-15 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "The Damsels of Cardigan", "Fair Tivy"; No. 16 of 26 Welsh songs +Title-Opus: WoO 155-16 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Dairy House", "A spreading hawthorn shades the seat"; No. 17 of 26 Welsh songs +Title-Opus: WoO 155-17 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Sweet Richard", "Yes, thou art chang’d since first we met"; No. 18 of 26 Welsh songs +Title-Opus: WoO 155-18 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Vale of Clwyd", "Think not I’ll leave"; No. 19 of 26 Welsh songs +Title-Opus: WoO 155-19 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "To the Blackbird", "Sweet warbler of a strain divine"; No. 20 of 26 Welsh songs +Title-Opus: WoO 155-20 +Title-Dates: 1813 + +Title-RAW: Folksong Setting "Cupid’s Kindness", "Dear brother"; No. 21 of 26 Welsh songs +Title-Opus: WoO 155-21 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Constancy", "Tho’ cruel fate should bid us part"; No. 22 of 26 Welsh songs +Title-Opus: WoO 155-22 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Old Strain", "My pleasant home beside the Dee!"; No. 23 of 26 Welsh songs +Title-Opus: WoO 155-23 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "Three Hundred Pounds", "In yonder snug cottage"; No. 24 of 26 Welsh songs +Title-Opus: WoO 155-24 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Parting Kiss", "Laura, thy sighs must now No. more"; No. 25 of 26 Welsh songs +Title-Opus: WoO 155-25 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Good Night", "Ere yet we slumber seek"; No. 26 of 26 Welsh songs +Title-Opus: WoO 155-26 +Title-Dates: 1810 + +Title-RAW: Folksong Setting "The Banner of Buccleuch", "From the brown crest of Newark"; No. 1 of 12 Scottish songs +Title-Opus: WoO 156-1 +Title-Dates: 1819 + +Title-RAW: Folksong Setting "Duncan Gray", "Duncan Gray came here to; woo"; No. 2 of 12 Scottish songs +Title-Opus: WoO 156-2 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "Up! quit thy bower"; No. 3 of 12 Scottish songs +Title-Opus: WoO 156-3 +Title-Dates: 1819 + +Title-RAW: Folksong Setting "Ye shepherds of this pleasant vale"; No. 4 of 12 Scottish songs +Title-Opus: WoO 156-4 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "Cease your funning"; No. 5 of 12 Scottish songs +Title-Opus: WoO 156-5 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "Highland Harry", "My Harry was a gallant gay"; No. 6 of 12 Scottish songs +Title-Opus: WoO 156-6 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Polly Stewart", "O lovely Polly Stewart"; No. 7 of 12 Scottish songs +Title-Opus: WoO 156-7 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "Womankind", "The hero may perish his country to save"; No. 8 of 12 Scottish songs +Title-Opus: WoO 156-8 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "Lochnagar", "Away ye gay landscapes"; No. 9 of 12 Scottish songs +Title-Opus: WoO 156-9 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "Glencoe", "O tell us, Harper"; No. 10 of 12 Scottish songs +Title-Opus: WoO 156-10 +Title-Dates: 1819 + +Title-RAW: Folksong Setting "Auld Lang Syne" "Should auld acquaintance be forgot"; No. 11 of 12 Scottish songs +Title-Opus: WoO 156-11 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "The Quaker’s Wife", "Dark was the morn and black the sea"; No. 12 of 12 Scottish songs +Title-Opus: WoO 156-12 +Title-Dates: 1818 + +Title-RAW: Folksong Setting "God Save the King", "God save our Lord the King" (English); No. 1 of 12 assorted folk songs +Title-Opus: WoO 157-1 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "The Soldier", "Then, Soldier! come" (Irish); No. 2 of 12 assorted folk songs +Title-Opus: WoO 157-2 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Charlie is my darling" (Scottish); No. 3 of 12 assorted folk songs +Title-Opus: WoO 157-3 +Title-Dates: 1819 + +Title-RAW: Folksong Setting "O sanctissima" (Sicilian); No. 4 of 12 assorted folk songs +Title-Opus: WoO 157-4 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "The Miller of Dee", "There was a jolly miller once" (English); No. 5 of 12 assorted folk songs +Title-Opus: WoO 157-5 +Title-Dates: 1819 + +Title-RAW: Folksong Setting "A health to the brave" (Irish); No. 6 of 12 assorted folk songs +Title-Opus: WoO 157-6 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Robin Adair", "Since all thy vows, false maid" (Irish); No. 7 of 12 assorted folk songs +Title-Opus: WoO 157-7 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "By the side of the Shannon" (Irish); No. 8 of 12 assorted folk songs +Title-Opus: WoO 157-8 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "Highlander’s Lament", "My Harry was a gallant gay" (Scottish); No. 9 of 12 assorted folk songs +Title-Opus: WoO 157-9 +Title-Dates: 1820 + +Title-RAW: Folksong Setting "Sir Johnnie Cope" (Scottish); No. 10 of 12 assorted folk songs +Title-Opus: WoO 157-10 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "The Wandering Minstrel", "I am bow’d down" (Irish); No. 11 of 12 assorted folk songs +Title-Opus: WoO 157-11 +Title-Dates: 1815 + +Title-RAW: Folksong Setting "La gondoletta", "La biondina in gondoletta" (Venetian); No. 12 of 12 assorted folk songs +Title-Opus: WoO 157-12 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Ridder Stigs Runer" (Danish); WoO 158a, No. 1 of 23 continental folk songs +Title-Opus: WoO 158-1 +Title-Dates: 1813,1817 + +Title-RAW: Folksong Setting "Horch auf, mein Liebchen" (German); WoO 158a, No. 2 of 23 continental folk songs +Title-Opus: WoO 158-2 +Title-Dates: 1813,1816,1817 + +Title-RAW: Folksong Setting "Wegen meiner blieb d’Fräula" (German); WoO 158a, No. 3 of 23 continental folk songs +Title-Opus: WoO 158-3 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting "Wann i in der Früh aufsteh" (Tyrolean); WoO 158a, No. 4 of 23 continental folk songs +Title-Opus: WoO 158-4 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting "Teppichkrämer-Lied"; "I bin a Tyroler Bua" (Tyrolean); WoO 158a, No. 5 of 23 continental folk songs +Title-Opus: WoO 158-5 +Title-Dates: 1815,1816,1818 + +Title-RAW: Folksong Setting "A Madel, ja a Madel" (Tyrolean); WoO 158a, No. 6 of 23 continental folk songs +Title-Opus: WoO 158-6 +Title-Dates: 1810,1815,1816 + +Title-RAW: Folksong Setting "Wer solche Buema afipackt" (Tyrolean); WoO 158a, No. 7 of 23 continental folk songs +Title-Opus: WoO 158-7 +Title-Dates: 1810,1817 + +Title-RAW: Folksong Setting "Ih mag di nit nehma, du töppeter Hecht" (Tyrolean); WoO 158a, No. 8 of 23 continental folk songs +Title-Opus: WoO 158-8 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "Oj, oj upilem sie w karczmie" (Polish); WoO 158a, No. 9 of 23 continental folk songs +Title-Opus: WoO 158-9 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Poszla baba po popiol" (Polish); WoO 158a, No. 10 of 23 continental folk songs +Title-Opus: WoO 158-10 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Yo No. quiero embarcarme" (Iberian); WoO 158a, No. 11 of 23 continental folk songs +Title-Opus: WoO 158-11 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Seus lindos olhos" (Portuguese); WoO 158a, No. 12 of 23 continental folk songs +Title-Opus: WoO 158-12 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Vo lesocke komarockov mnogo urodilos" (Im Walde sind viele Mücklein geboren) (Russian); WoO 158a, No. 13 of 23 continental folk songs +Title-Opus: WoO 158-13 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Akh, recen’ki, recen’ki" (Ach Bächlein, Bächlein, kühle Wasser) (Russian); WoO 158a, No. 14 of 23 continental folk songs +Title-Opus: WoO 158-14 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Kak Posli nasi podruzki" (As They Went) (Unsere Mädchen gingen in den Wald) (Russian); WoO 158a, No. 15 of 23 continental folk songs +Title-Opus: WoO 158-15 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Schöne Minka, ich muß scheiden" (Ukrainian-Cossack); WoO 158a, No. 16 of 23 continental folk songs +Title-Opus: WoO 158-16 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Lilla Carl", Vaggvisa (Swedish lullaby); WoO 158a, No. 17 of 23 continental folk songs +Title-Opus: WoO 158-17 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "An ä Bergli bin i gesässe" (Swiss); WoO 158a, No. 18 of 23 continental folk songs +Title-Opus: WoO 158-18 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Una paloma blanca" (Spanish Bolero a Solo); WoO 158a, No. 19 of 23 continental folk songs +Title-Opus: WoO 158-19 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Como la mariposa soy" (Spanish Bolero a due); WoO 158a, No. 20 of 23 continental folk songs +Title-Opus: WoO 158-20 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Tiranilla Española" (Spanish); "La tiranna se embarca"; WoO 158a, No. 21 of 23 continental folk songs +Title-Opus: WoO 158-21 +Title-Dates: 1816 + +Title-RAW: Folksong Setting "Édes kinos emlékezet", "Magyar Szüretötö Enek" (Hungarian grape harvest song); WoO 158a, No. 22 of 23 continental folk songs +Title-Opus: WoO 158-22 +Title-Dates: 1817 + +Title-RAW: Folksong Setting "Da brava, Catina" (Venetian); WoO 158a, No. 23 of 23 continental folk songs +Title-Opus: WoO 158-23 +Title-Dates: 1816 + +Title-RAW: Folksong Setting +Title-Key: A major +Title-Name: Adieu, my lov’d harp +Title-Comment: (Irish); WoO 158b, No. 1 of 7 British folk songs +Title-Opus: WoO 158-1 +Title-Dates: 1813,1817 + +Title-RAW: Folksong Setting +Title-Key: E flat major +Title-Name: Castle O’Neill +Title-Comment: (Irish), without words; WoO 158b, No. 2 of 7 British folk songs +Title-Opus: WoO 158-2 +Title-Dates: 1813,1816,1817 + +Title-RAW: Folksong Setting "Oh ono chri!" (Scottish) "O was not I a weary wight"; WoO 158b, No. 3 of 7 British folk songs +Title-Opus: WoO 158-3 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting "Red gleams the sun on yon hill tap" (Scottish); WoO 158b, No. 4 of 7 British folk songs +Title-Opus: WoO 158-4 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting +Title-Key: G major +Title-Name: Erin! O Erin! +Title-Comment: (Scottish-Irish) "Like the bride lamp that lay"; WoO 158b, No. 5 of 7 British folk songs +Title-Opus: WoO 158-5 +Title-Dates: 1815,1816,1818 + +Title-RAW: Folksong Setting "O Mary ye’s be clad in silk" (Scottish); WoO 158b, No. 6 of 7 British folk songs +Title-Opus: WoO 158-6 +Title-Dates: 1810,1815,1816 + +Title-RAW: Folksong Setting "Lament for Owen Roe O’Neill" (Irish) without words; WoO 158b, No. 7 of 7 British folk songs +Title-Opus: WoO 158-7 +Title-Dates: 1810,1817 + +Title-RAW: Folksong Setting +Title-Key: E minor +Title-Name: When my Hero in court appears +Title-Punct: ; +Title-Comment: WoO 158c, No. 1 of 6 assorted folk songs +Title-Opus: WoO 158-1 +Title-Dates: 1813,1817 + +Title-RAW: Folksong Setting +Title-Key: B major +Title-Name: Air de Colin +Title-Comment: from Rousseau’s "Le Devin du Village"; "Non, non, Colette n’est point trompeuse"; WoO 158c, No. 2 of 6 assorted folk songs +Title-Opus: WoO 158-2 +Title-Dates: 1813,1816,1817 + +Title-RAW: Folksong Setting +Title-Key: B major +Title-Name: Mark yonder pomp of costly fashion +Title-Comment: (Scottish); WoO 158c, No. 3 of 6 assorted folk songs +Title-Opus: WoO 158-3 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting +Title-Key: A major +Title-Name: Bonnie wee thing +Title-Comment: (Scottish) for 3 voices and piano; WoO 158c, No. 4 of 6 assorted folk songs +Title-Opus: WoO 158-4 +Title-Dates: 1816,1817,1820 + +Title-RAW: Folksong Setting +Title-Key: B flat major +Title-Name: From thee, Eliza, I must go +Title-Comment: (Scottish) for 3 voices and piano trio; WoO 158c, No. 5 of 6 assorted folk songs +Title-Opus: WoO 158-5 +Title-Dates: 1815,1816,1818 + +Title-RAW: Folksong Setting +Title-Key: E minor +Title-Punct: : +Title-Comment: Untitled (Scottish) without words; WoO 158c, No. 6 of 6 assorted folk songs +Title-Opus: WoO 158-6 +Title-Dates: 1810,1815,1816 + +Title-RAW: Folksong Setting +Title-Key: F major +Title-Name: Air Français +Title-Comment: (French) +Title-Opus: WoO 158d +Title-Opus: WoO 158 + +Title-Type: 3-part Canon +Title-Key: F major +Title-Name: Im Arm der Liebe ruht sich’s wohl +Title-Opus: WoO 159 +Title-Dates: 1795 + +Title-Type: 3-part Canon +Title-Key: G major +Title-Punct: : +Title-Comment: for Three Unison Voices (Allegretto) without Text +Title-Opus: WoO 160-1 +Title-Dates: 1795 + +Title-RAW: 4-part Canon: for Four Unison Voices (Moderato) without Text +Title-Opus: WoO 160-2 +Title-Dates: 1795 + +Title-Type: 3-part Canon +Title-Key: C major +Title-Name: Ewig dein! +Title-Comment: (Forever thine!) +Title-Opus: WoO 161 +Title-Dates: 1811 + +Title-Type: 4-part Canon +Title-Key: B flat major +Title-Name: Ta ta ta, lieber Mälzel +Title-Punct: ; +Title-Comment: authorship: spurious +Title-Opus: WoO 162 +Title-Dates: 1812 + +Title-Type: 3-part Canon +Title-Key: F minor +Title-Name: Kurz ist der Schmerz, und ewig ist die Freude +Title-Comment: (Suffering is transitory, but joy is eternal) (1st setting) +Title-Opus: WoO 163 +Title-Dates: 1813 + +Title-Type: 3-part Canon +Title-Key: C major +Title-Name: Freundschaft ist die Quelle wahrer Glückseligkeit +Title-Comment: (Friendship is the source of true bliss) +Title-Opus: WoO 164 +Title-Dates: 1814 + +Title-RAW: 4-part Canon "Glück zum neuen Jahr!" (1st setting) +Title-Opus: WoO 165 +Title-Dates: 1815 + +Title-Type: 3-part Canon +Title-Key: F major +Title-Name: Kurz ist der Schmerz, und ewig ist die Freude +Title-Comment: (Suffering is transitory, but joy is eternal) (2nd setting) +Title-Opus: WoO 166 +Title-Dates: 1815 + +Title-Type: 3-part Canon +Title-Key: C major +Title-Name: Brauchle, Linke +Title-Opus: WoO 167 +Title-Dates: 1815 + +Title-Type: 3-part Riddle Canon +Title-Key: F major +Title-Name: Das Schweigen +Title-Alternative-Name: Lerne Schweigen, o Freund +Title-Opus: WoO 168-1 +Title-Dates: 1816 + +Title-Type: 3-part Canon +Title-Key: F major +Title-Name: Das Reden +Title-Alternative-Name: Rede, wenn’s um einen Freund dir gilt +Title-Opus: WoO 168-2 +Title-Dates: 1816 + +Title-Type: 2-part Riddle Canon +Title-Key: C major +Title-Name: Ich küße Sie, drücke Sie an mein Herz +Title-Opus: WoO 169 +Title-Dates: 1816 + +Title-Type: 2-part Canon +Title-Key: C major +Title-Name: Ars longa, vita brevis +Title-Comment: (first version) +Title-Opus: WoO 170 +Title-Dates: 1816 + +Title-Type: 4-part Canon +Title-Key: G major +Title-Name: Glück fehl’ dir vor allem! +Title-Punct: ; +Title-Comment: authorship: spurious +Title-Opus: WoO 171 +Title-Dates: 1817 + +Title-Type: 3-part Canon +Title-Key: E flat major +Title-Name: Ich bitt’ dich, schreib’ mir die Es-Scala auf +Title-Opus: WoO 172 +Title-Dates: 1818 + +Title-Type: 2-part Riddle Canon +Title-Key: B flat major +Title-Name: Hol’ euch der Teufel! B’hüt euch Gott! +Title-Opus: WoO 173 +Title-Dates: 1819 + +Title-Type: 4-part Canon +Title-Key: B flat major +Title-Name: Glaube und hoffe +Title-Opus: WoO 174 +Title-Dates: 1819 + +Title-Type: 4-part Riddle Canon +Title-Name: Sankt Petrus war ein Fels/ Bernardus war ein Sankt +Title-Opus: WoO 175 +Title-Dates: 1820 + +Title-Type: 3-part Canon +Title-Key: F major +Title-Name: Glück zum neuen Jahr! +Title-Comment: (2nd setting) +Title-Opus: WoO 176 +Title-Dates: 1819 + +Title-Type: Canon +Title-Key: E major +Title-Name: Bester Magistrat, Ihr friert +Title-Punct: ; +Title-Comment: canon for 2 male voices & 2 double-basses +Title-Opus: WoO 177 +Title-Dates: 1820 + +Title-Type: 3-part Canon +Title-Key: B flat major +Title-Name: Signor Abate +Title-Opus: WoO 178 +Title-Dates: 1820 + +Title-RAW: Intro & 4-part Canon +Title-Key: C major +Title-Name: Seiner Kaiserlichen Hoheit...alles Gute, alles Schöne +Title-Opus: WoO 179 +Title-Dates: 1819 + +Title-Type: 2-part Canon +Title-Key: C major +Title-Name: Auf einen, welcher Hoffmann geheißen +Title-Alternative-Name: Hoffmann, sei ja kein Hofmann +Title-Opus: WoO 180 +Title-Dates: 1820 + +Title-Type: 4-part Canon +Title-Key: C major +Title-Name: Gedenket heute an Baden +Title-Opus: WoO 181-1 +Title-Dates: 1820 + +Title-Type: 4-part Canon +Title-Key: C major +Title-Name: Gehabt euch wohl +Title-Opus: WoO 181-2 +Title-Dates: 1820 + +Title-Type: 3-part Canon +Title-Key: C major +Title-Name: Tugend ist kein leerer Name +Title-Opus: WoO 181-3 +Title-Dates: 1820 + +Title-Type: 3-part Canon +Title-Key: D minor +Title-Name: O Tobias! +Title-Opus: WoO 182 +Title-Dates: 1821 + +Title-Type: 4-part Canon +Title-Key: F major +Title-Name: Bester Herr Graf, Sie sind ein Schaf +Title-Opus: WoO 183 +Title-Dates: 1823 + +Title-Type: 5-part Canon +Title-Key: G major +Title-Name: Falstafferel, lass dich sehen! +Title-Opus: WoO 184 +Title-Dates: 1823 + +Title-Type: 6-part Canon +Title-Name: Edel sei der Mensch, hülfreich und gut +Title-Opus: WoO 185 +Title-Dates: 1823 + +Title-Type: 2-part Canon +Title-Key: E flat major +Title-Name: Te solo adoro +Title-Opus: WoO 186 +Title-Dates: 1824 + +Title-Type: 4-part Canon +Title-Key: F major +Title-Name: Auf einen, welcher Schwenke geheißen +Title-Alternative-Name: Schwenke dich ohne Schwänke +Title-Opus: WoO 187 +Title-Dates: 1824 + +Title-Type: 2-part Riddle Canon +Title-Key: B flat major +Title-Name: Gott ist eine feste Burg +Title-Opus: WoO 188 +Title-Dates: 1825 + +Title-Type: 4-part Canon +Title-Key: C major +Title-Name: Doktor, sperrt das Tor dem Tod +Title-Opus: WoO 189 +Title-Dates: 1825 + +Title-Type: 2-part Canon +Title-Key: C major +Title-Name: Ich war hier, Doktor, ich war hier +Title-Opus: WoO 190 +Title-Dates: 1825 + +Title-Type: 3-part Canon +Title-Key: B flat major +Title-Name: Kühl, nicht lau +Title-Opus: WoO 191 +Title-Dates: 1825 + +Title-Type: 4-part Riddle Canon +Title-Key: F major +Title-Name: Ars longa, vita brevis +Title-Comment: (second version) +Title-Opus: WoO 192 +Title-Dates: 1825 + +Title-Type: Riddle Canon +Title-Key: C major +Title-Name: Ars longa, vita brevis +Title-Comment: (third version) +Title-Opus: WoO 193 +Title-Dates: 1825 + +Title-Type: Riddle Canon +Title-Key: F major +Title-Name: Si non per portas, per muros +Title-Opus: WoO 194 +Title-Dates: 1825 + +Title-Type: 2-part Canon +Title-Key: A minor +Title-Name: Freu dich des Lebens +Title-Opus: WoO 195 +Title-Dates: 1825 + +Title-Type: 4-part Riddle Canon +Title-Key: F major +Title-Name: Es muß sein! +Title-Opus: WoO 196 +Title-Dates: 1826 + +Title-Type: 5-part Canon +Title-Key: C major +Title-Name: Das ist das Werk, sorgt um das Geld! +Title-Opus: WoO 197 +Title-Dates: 1826 + +Title-Type: 2-part Riddle Canon +Title-Key: C major +Title-Name: Wir irren allesamt +Title-Opus: WoO 198 +Title-Dates: 1826 + +Title-Type: Joke +Title-Key: D major +Title-Name: Ich bin der Herr von zu +Title-Comment: (I am the man for you) +Title-Opus: WoO 199 +Title-Dates: 1814 + +Title-Type: Piece +Title-For: Piano +Title-Key: G major +Title-Name: O Hoffnung! +Title-Opus: WoO 200 +Title-Dates: 1818 + +Title-Type: Joke +Title-Key: C major +Title-Name: Ich bin bereit! Amen +Title-Comment: (I am ready), beginning of a double fugue +Title-Opus: WoO 201 +Title-Dates: 1818 + +Title-Type: Riddle Canon +Title-Key: F major +Title-Name: Das Schöne zum Guten! +Title-Punct: , +Title-Comment: musical motto (1st version) +Title-Opus: WoO 202 +Title-Dates: 1823 + +Title-Type: Riddle Canon +Title-Key: A major +Title-Name: Das Schöne zu dem Guten! +Title-Punct: , +Title-Comment: musical motto (2nd version) +Title-Opus: WoO 203 +Title-Dates: 1825 + +Title-Type: Joke +Title-Key: D minor +Title-Name: Holz, Holz, geigt die Quartette so +Title-Comment: (Holz, Holz, you play the quartets as if you were chopping cabbage) +Title-Opus: WoO 204 +Title-Dates: 1825 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Baron, Baron, Baron +Title-Punct: , +Title-Comment: No. 1 of 10 musical greetings +Title-Opus: WoO 205-1 +Title-Dates: 1798 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Allein, allein, allein, jedoch. Silentium!! +Title-Punct: , +Title-Comment: No. 2 of 10 musical greetings +Title-Opus: WoO 205-2 +Title-Dates: 1814 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: A minor +Title-Name: O Adjutant +Title-Punct: , +Title-Comment: No. 3 of 10 musical greetings +Title-Opus: WoO 205-3 +Title-Dates: 1817 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Wo? Wo? +Title-Punct: , +Title-Comment: No. 4 of 10 musical greetings +Title-Opus: WoO 205-4 +Title-Dates: 1817 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: D major +Title-Name: Erfüllung, Erfüllung +Title-Punct: , +Title-Comment: No. 5 of 10 musical greetings +Title-Opus: WoO 205-5 +Title-Dates: 1819 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: F major +Title-Name: Scheut euch nicht +Title-Punct: , +Title-Comment: No. 6 of 10 musical greetings +Title-Opus: WoO 205-6 +Title-Dates: 1822 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: E major +Title-Name: Tobias! Paternostergäßler. Tobias! Paternostergäßlerischer, Bierhäuslerischer musikalischer Philister! +Title-Punct: , +Title-Comment: No. 7 of 10 musical greetings +Title-Opus: WoO 205-7 +Title-Dates: 1824 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: D major +Title-Name: Tobias Tobias +Title-Punct: , +Title-Comment: No. 8 of 10 musical greetings +Title-Opus: WoO 205-8 +Title-Dates: 1825 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Bester To----(bias) +Title-Punct: , +Title-Comment: No. 9 of 10 musical greetings +Title-Opus: WoO 205-9 +Title-Dates: 1826 + +Title-Type: Musical greetings +Title-For: Solo Voice(s) +Title-Key: C major +Title-Name: Erster aller Tobiasse +Title-Punct: , +Title-Comment: No. 10 of 10 musical greetings +Title-Opus: WoO 205-10 +Title-Dates: 1826 + diff --git a/fhem/FHEM/lib/Normalize/Text/Music_Fields/Music_Fields-rus.lst b/fhem/FHEM/lib/Normalize/Text/Music_Fields/Music_Fields-rus.lst new file mode 100644 index 000000000..973ff624d --- /dev/null +++ b/fhem/FHEM/lib/Normalize/Text/Music_Fields/Music_Fields-rus.lst @@ -0,0 +1,218 @@ +# charset = cp1251 + +### Aliases should be at the front; correct => misspell1, misspell2... +# alias ׸ðíûé => ×åðíûé +# alias Êóøåë¸â-Áåçáîðîäêî => Êóøåëåâ-Áåçáîðîäêî +# alias Êóøåë¸â-Áåçáîðîäêî => Êóøåëåâ-Áåçáîðîäêî +# alias Ãóìèë¸â => Ãóìèëåâ + +Àëåêñàíäð Äîëüñêèé +Àëåêñàíäð Ìèðçàÿí +Àëåêñàíäð Ñóõàíîâ +Àëåêñàíäð Âåðòèíñêèé (1889-03-21--1957-05-21) +Àëåêñàíäð Ãîðîäíèöêèé +Áóëàò Îêóäæàâà (1924-5-9--1997-6-12) +Åëåíà Êàìáóðîâà +Ìèõàèë Ùåðáàêîâ +Íîâåëëà Ìàòâååâà +Âåðà Ìàòâååâà (1945-10-23--1976-8-11) +Àäà (Àðèàäíà Àäàìîâíà) ßêóøåâà +Âëàäèìèð Ñåðãååâè÷ Äàøêåâè÷ + +## First prefered: +Ñåðãåé è Òàòüÿíà Íèêèòèíû +##Òàòüÿíà è Ñåðãåé Íèêèòèíû + +Ñåðãåé Íèêèòèí +Âàëåðèé Àãàôîíîâ (1941-10-3-1984-9-5) +Âèêòîð Áåðêîâñêèé (1932-07-13--2005-07-22) +Âëàäèìèð Âûñîöêèé (1938-1-25--1980-7-25) +Þëèé Êèì +ôîëüêëîð ÌÃÓ +Äìèòðèé Ñóõàðåâ +Àðñåíèé Òàðêîâñêèé (1907-6-24--1989-5-27) +Àãíèÿ Áàðòî (1906-2-17--1981-4-1) +Âàäèì Âëàäèìèðîâè÷ Åãîðîâ +Èâàí Ñåìåíîâè÷ Êèóðó +Ãðèãîðèé Ïîæåíÿí +Ëàðèñà Êðèòñêàÿ + +Àíäðåé Âîçíåñåíñêèé +Àëåêñàíäð Êóøíåð +Áîðèñ Ëåîíèäîâè÷ Ïàñòåðíàê (1890-2-10--1960-5-30) +Îñèï Ýìèëüåâè÷ Ìàíäåëüøòàì (1891-1-15--1938-12-27) +Âëàäèìèð Âëàäèìèðîâè÷ Ìàÿêîâñêèé (1893-07-19--1930-4-14) +Àíäðåé Ìèðîíîâ (1941-3-8--1987-8-16) +Àíäðåé Ïåòðîâ +Àëåêñàíäð Áëîê (1880-11-28--1921-8-7) +Áîðèñ Âëàäèìèðîâè÷ Çàõîäåð (1918-9-9-2000-11-7) +Áîðèñ Íàòàíîâè÷ Ñòðóãàöêèé +Áîðèñ Ñëóöêèé (1919-5-7--1986-2-22) +Þðèé Âèçáîð (1934-6-20--1984-9-17) +Þðèé Äàâûäîâè÷ Ëåâèòàíñêèé (1922-1-21--1996-1-24) +Àäà ßêóøåâà +Àëåêñàíäð Ãàëè÷ (1918-10-19--1977-12-15) +Àëåêñàíäð Äóëîâ +Âåðîíèêà Òóøíîâà (1915-1965) +Àíäðåé Ìàêàðåâè÷ +Áîðèñ Ãðåáåíùèêîâ +Àëåêñàíäð Êàðïîâ +Àíäðåé Êîðô +Äîìèíî +Èãîðü Áåëûé +Êèðèëë Âîëîøèí +Ëåîíèä Ñåðãååâ +Íàòàëüÿ Ïðèåçæåâà +Îëåã Ìèòÿåâ +Ñêàé +Ñâåòëàíà Âåòðîâà +Òàíÿ Êîðîëåâà +Òèìóð Øàîâ +Òðè Ñó÷êà +Âàäèì è Âàëåðèé Ìèùóêè +Âåðîíèêà Äîëèíà +Âëàäèìèð Ëàíöáåðã + +## This causes infinite loop of expansion Èâàùåíêî => Âàñèëüåâ è Èâàùåíêî => Èâàùåíêî +## Âàñèëüåâ è Èâàùåíêî + +Ìèõàèë Ñâåòëîâ (1903-6-17--1964-7-28) +Ýäóàðä Áàãðèöêèé (1895-11-4--1939-2-16) +Âèêòîð Ñîñíîðà +Íèêîëàé Çàáîëîöêèé (1903-5-7--1958-10-14) +Ãåîðãèé Èâàíîâ (1894-11-10--1958-8-26) +Äàíèèë Õàðìñ (1905-12-30--1942-2-2) +Äîí Àìèíàäî (1888-5-7--1957-11-14) +Åâãåíèé Åâòóøåíêî +Åâãåíèé Êëÿ÷êèí (1934-3-23--1994-7-30) +Èîñèô Àëåêñàíäðîâè÷ Áðîäñêèé (1940-5-24--1996-1-28) +Îëåã ×óõîíöåâ +Ïàâåë Êîãàí (1918-7-7--1942) +Ïîëü Âåðëåí (1844-3-30--1896-1-8) +Þðèé Àäåëóíã (1945-4-3--1993-1-6) +Äàâèä Ñàìóèëîâè÷ Ñàìîéëîâ (1920-6-1--1990-2-23) +Ãåíðèõ Âåíèàìèíîâè÷ Ñàïãèð (1928-11-20--1999-10-7) +Îâñåé Äðèç (1908-5-16--1971-2-14) +Èâàí Êðûëîâ (1769-2-13--1844-11-21) +Ñàìóèë ßêîâëåâè÷ Ìàðøàê (1887-11-3--1964-7-4) +Êîíñòàíòèí Áèáë (1898-2-26--1951-11-12) +Ìàðèíà Èâàíîâíà Öâåòàåâà (1992-10-8--1941-8-31) +Ñàøà ׸ðíûé (1880-10-13--1932-7-5) +Âåíèàìèí Áîðèñîâè÷ Ñìåõîâ +Ìàêñèìèëèàí Àëåêñàíäðîâè÷ Âîëîøèí (1877-5-28--1932-8-11) +Íèêîëàé Ñòåïàíîâè÷ Ãóìèë¸â (1886-4-15--1921-8) +Ãðèãîðèé Àëåêñàíäðîâè÷ Êóøåë¸â-Áåçáîðîäêî (1832-2-1--1870-5-13), +ϸòð Àíäðååâè÷ Âÿçåìñêèé (1792-7-23--1878-11-22) +Ìèõàèë Ëüâîâè÷ Ìàòóñîâñêèé (1915-7-23--1990-7-16) +Àëåêñàíäð Àëåêñàíäðîâè÷ Àëÿáüåâ (1787-8-15--1851-3-6) +Áîðèñ Àëåêñååâè÷ Ïðîçîðîâñêèé (1891-6-30--1937) + +## Maurice Car^eme +Ìîðèñ Êàðåì (1899-5-12--1978-1-13) + +## dates not checked +Ìèõàèë Êóäèìîâ +Áîðèñ Íîñèê +Âàäèì Åãîðîâ +Òèì Ñîáàêèí +Þðèé Êóçíåöîâ +Àãíåøêà Îñååöêàÿ + +À. Í. Îñòðîâñêèé +Àíòîí Ïàâëîâè÷ ×åõîâ (1860-1-29--1904-7-15) +Àëåêñàíäð Ñåðãååâè÷ Ãðèáîåäîâ (1790(5?)-1-15-1829-2-11) +Àëåêñàíäð Ñåðãååâè÷ Ïóøêèí (1799-6-6-1837-2-10) +Àíäðå Ìîðóà (1885-1967) +Àðêàäèé Àâåð÷åíêî (1881-3-18--1925-3-18) +Âàñèëü Áûêîâ (1924-6-19--2003-6-22) +Âëàäèìèð Àëåêñååâè÷ Ãèëÿðîâñêèé (1853-12-8---1935-10-1) +Äåíèñ Èâàíîâè÷ Ôîíâèçèí (1744(5?)-4-3--1792) +Äæåê Ëîíäîí (1876-1-12--1916-11-22) +Èâàí Àëåêñååâè÷ Áóíèí (1870-10-22--1953-11-8) +Èëüÿ Èëüô (1897-10-15--1937-4-12) +Åâãåíèé Ïåòðîâ (1903-12-13--1942-7) +Èâàí Ñåðãååâè÷ Òóðãåíåâ (1818-10-28--1883-11-22) +Èñààê Áàáåëü (1894-7-13-1940-1-27) +Ëåâ Íèêîëàåâè÷ Òîëñòîé (1828-9-9--1910-11-20) +Ìèõàèë Àôàíàñüåâè÷ Áóëãàêîâ (1891-5-15--1940-3-10) +Ìèõàèë Þðüåâè÷ Ëåðìîíòîâ (1814-10-15--1841-7-27) +Íèêîëàé Âàñèëüåâè÷ Ãîãîëü (1809-4-1--1852-3-4) +Íèêîëàé Ñåì¸íîâè÷ Ëåñêîâ (1831-2-16--1895-3-5) +Î. Ãåíðè (1862-7-11--1910-6-5) +Îñêàð Óàéëüä (1984-10-16--1900-11-13) +Ô¸äîð Ìèõàéëîâè÷ Äîñòîåâñêèé (1821-11-11--1881-2-9) +Ãàíñ Õðèñòèàí Àíäåðñåí (1805-4-2--1875-8-4) +ßðîñëàâ Ãàøåê (1883-4-30--1923-3-3) +Äæîçåô Ðåäüÿðä Êèïëèíã (1865-12-30--1936-1-18) + +Þðèé Àäåëóíã (1945-4-3--1993-1-6) +Àëåêñåé Íèêîëàåâè÷ Àïóõòèí (1840(1?)-11-27--1893-8-29) +Àôàíàñèé Àôàíàñüåâè÷ Ôåò (1820-12-5--1892-12-3) +Àëåêñåé Êîíñòàíòèíîâè÷ Òîëñòîé (1817-9-5--1875-10-10) +Âëàäèìèð Âëàäèìèðîâè÷ Íàáîêîâ (1899-4-24--1977-7-2) +Ðîáåðò Èâàíîâè÷ Ðîæäåñòâåíñêèé (1932-6-20--1994-3-20) +Âëàäèìèð Òóðèÿíñêèé +Âàäèì Ñåðãååâè÷ Øåôíåð (1915-2002) +Èííîêåíòèé Ô¸äîðîâè÷ Àííåíñêèé (1855-9-1--1909-12-11) +Èëüÿ Ëüâîâè÷ Ñåëüâèíñêèé (1899-10-24--1968-3-22) +Èñààê Èîñèôîâè÷ Øâàðö +Èëüÿ Ãðèãîðüåâè÷ Ýðåíáóðã (1891-1-27--1967-8-31) +Ìèõàèë Ñåðãååâè÷ Áîÿðñêèé +Ìàêñèìèëèàí Àëåêñàíäðîâè÷ Âîëîøèí (1877-5-28-1932-8-11) +Ìèêàýë Òàðèâåðäèåâ (1931-8-15--1996-6-24) +Íèêîëàé Ìèõàéëîâè÷ Ðóáöîâ (1936-1-3--1971-1-19) +Ôåäîð Èâàíîâè÷ Òþò÷åâ (1803-12-5--1873-7-27) +Íèêîëîç Ìåëèòîíîâè÷ Áàðàòàøâèëè (1817-12-27--1845-10-21) +Âåðà Èëüèíè÷íà Ìàòâååâà (1945-10-23--1976-8-11) +Ãåííàäèé Ô¸äîðîâè÷ Øïàëèêîâ (1937-09-06--1974-11-01) +Àëåêñàíäð Ìîèñååâè÷ Âîëîäèí (1919-2-10--2001-12-17) +Þðèé Àáðàìîâè÷ Ëåâèòèí (1912-12-18--1993-8-2) +Ñåðãåé Àëåêñàíäðîâè÷ Åñåíèí (1895-10-3--1925-12-28) + +## Â. Áåðåñòîâ +## Â. Çîëîòóõèí +## Â. Ñìåõîâ + +Îëåã Àíîôðèåâ +Îëåã Èâàíîâè÷ Äàëü (1941-5-25--1981-5-3) +Ïåðñè Áèøè Øåëëè (1792-8-4--1822-7-8) +Ðèììà Ô¸äîðîâíà Êàçàêîâà +Ýëüäàð Ðÿçàíîâ +Ýäóàðä Íèêîëàåâè÷ Óñïåíñêèé +Þðèé Àëåêñååâè÷ Êóêèí +Þííà Ïåòðîâíà Ìîðèö + +## ß. Ïðèãîæèé + +### These should be at the end + +### Possible misspellings of the first name +# fix_firstname ôîëêëîð ÌÃÓ + +### Misspelling of older versions fixed: misspelled => correct_shortcut +# fix Ivan Krylov (1769-1844) => Êðûëîâ +# fix Samuil Marshak (1887-1964) => Ìàðøàê +# fix À. Ñ. Ãðèáîåäîâ (1790(5?)-1829) => Àëåêñàíäð Ñåðãååâè÷ Ãðèáîåäîâ (1790(5?)-1-15-1829-2-11) +# fix Ä. È. Ôîíâèçèí (1744(5?)-1792) => Äåíèñ Èâàíîâè÷ Ôîíâèçèí (1744(5?)-4-3--1792) +# fix Õ. Ê. Àíäåðñåí => Àíäåðñåí +# fix Ï.-Á. Øåëëè => Øåëëè +# fix Ýäóàðä Áàãðèöêèé (1922-1942) => Áàãðèöêèé +# fix Èîñèô Àëåêñàíäðîâè÷ Áðîäñêèé (19405-24--1996-1-28) => Áðîäñêèé +# fix Òàòüÿíà è Ñåðãåé Íèêèòèíû => Ñåðãåé è Òàòüÿíà Íèêèòèíû +# fix Ð. Êèïëèíã => Êèïëèíã + +## Non-automatic shortnames +# shortname Äì. Ñóõàðåâ +# shortname Íèêèòèíû +# shortname Àðñ. Òàðêîâñêèé +# shortname È. Êèóðó +# shortname À. Àïóõòèí +# shortname Äæ. Êèïëèíã +# shortname Äæ. Ëîíäîí + +## This might be wrongly expanded when combinations like +## Ñåðãåé è Òàòüÿíà Íèêèòèíû, Âèêòîð Áåðêîâñêèé +## are broken over "," and "è" + +# keep Òàòüÿíà Íèêèòèíû +# keep Ñåðãåé Íèêèòèíû diff --git a/fhem/FHEM/lib/UPnP/Common.pm b/fhem/FHEM/lib/UPnP/Common.pm new file mode 100644 index 000000000..9e0871833 --- /dev/null +++ b/fhem/FHEM/lib/UPnP/Common.pm @@ -0,0 +1,828 @@ +package UPnP::Common; + +use 5.006; +use strict; +use warnings; + +use HTTP::Headers; +use IO::Socket; + +use vars qw(@EXPORT $VERSION @ISA $AUTOLOAD); + +require Exporter; + +our @ISA = qw(Exporter); +our $VERSION = '0.03'; + +my %XP_CONSTANTS = ( + SSDP_IP => "239.255.255.250", + SSDP_PORT => 1900, + CRLF => "\015\012", + IP_LEVEL => getprotobyname('ip') || 0, +); + +#ALW - Changed from 'MSWin32' => [3,5], +my @MD_CONSTANTS = qw(IP_MULTICAST_TTL IP_ADD_MEMBERSHIP); +my %MD_CONSTANT_VALUES = ( + 'MSWin32' => [10,12], + 'cygwin' => [3,5], + 'darwin' => [10,12], + 'linux' => [33,35], + 'default' => [33,35], +); + +@EXPORT = qw(); + +use constant PROBE_IP => "239.255.255.251"; +use constant PROBE_PORT => 8950; + +my $ref = $MD_CONSTANT_VALUES{$^O}; +if (!defined($ref)) { + $ref = $MD_CONSTANT_VALUES{default}; +} +my $consts; +for my $name (keys %XP_CONSTANTS) { + $consts .= "use constant $name => \'" . $XP_CONSTANTS{$name} . "\';\n"; +} +for my $index (0..$#MD_CONSTANTS) { + my $name = $MD_CONSTANTS[$index]; + $consts .= "use constant $name => \'" . $ref->[$index] . "\';\n"; +} + +#warn $consts; # for development +eval $consts; +push @EXPORT, (keys %XP_CONSTANTS, @MD_CONSTANTS); + +#findLocalIP(); + +my %typeMap = ( + 'ui1' => 'int', + 'ui2' => 'int', + 'ui4' => 'int', + 'i1' => 'int', + 'i2' => 'int', + 'i4' => 'int', + 'int' => 'int', + 'r4' => 'float', + 'r8' => 'float', + 'number' => 'float', + 'fixed' => 'float', + 'float' => 'float', + 'char' => 'string', + 'string' => 'string', + 'date' => 'timeInstant', + 'dateTime.tz' => 'timeInstant', + 'time' => 'timeInstant', + 'time.tz' => 'timeInstant', + 'boolean' => 'boolean', + 'bin.base64' => 'base64Binary', + 'bin.hex' => 'hexBinary', + 'uri' => 'uriReference', + 'uuid' => 'string', +); + +BEGIN { + use SOAP::Lite; + $SOAP::Constants::DO_NOT_USE_XML_PARSER = 1; +} + +sub getLocalIP { + if (defined $UPnP::Common::LocalIP) { + return $UPnP::Common::LocalIP; + } + + my $probeSocket = IO::Socket::INET->new(Proto => 'udp', + Reuse => 1); + + my $listenSocket = IO::Socket::INET->new(Proto => 'udp', + Reuse => 1, + LocalPort => PROBE_PORT); + my $ip_mreq = inet_aton(PROBE_IP) . INADDR_ANY; + setsockopt($listenSocket, + getprotobyname('ip'), + $ref->[1], + $ip_mreq); + + my $destaddr = sockaddr_in(PROBE_PORT, inet_aton(PROBE_IP)); + send($probeSocket, "Test", 0, $destaddr); + + my $buf = ''; + my $peer = recv($listenSocket, $buf, 2048, 0); + my ($port, $addr) = sockaddr_in($peer); + + $probeSocket->close; + $listenSocket->close; + + setLocalIP($addr); + return $UPnP::Common::LocalIP; +} + +sub setLocalIP { + my ($addr) = @_; + $UPnP::Common::LocalIP = inet_ntoa($addr); +} + +sub parseHTTPHeaders { + my $buf = shift; + my $headers = HTTP::Headers->new; + + # Header parsing code borrowed from HTTP::Daemon + my($key, $val); + HEADER: + while ($buf =~ s/^([^\012]*)\012//) { + $_ = $1; + s/\015$//; + if (/^([^:\s]+)\s*:\s*(.*)/) { + $headers->push_header($key => $val) if $key; + ($key, $val) = ($1, $2); + } + elsif (/^\s+(.*)/) { + $val .= " $1"; + } + else { + last HEADER; + } + } + $headers->push_header($key => $val) if $key; + + return $headers; +} + +sub UPnPToSOAPType { + my $upnpType = shift; + return $typeMap{$upnpType}; +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::DeviceLoader; + +use strict; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + return bless { + _parser => UPnP::Common::Parser->new, + }, $class; +} + +sub parser { + my $self = shift; + return $self->{_parser}; +} + +sub parseServiceElement { + my $self = shift; + my $element = shift; + my($name, $attrs, $children) = @$element; + + my $service = $self->newService(%{$_[1]}); + for my $childElement (@$children) { + my $childName = $childElement->[0]; + + if (UPnP::Common::Service::isProperty($childName)) { + my $value = $childElement->[2]; + $service->$childName($value); + } + } + + return $service; +} + +sub parseDeviceElement { + my $self = shift; + my $element = shift; + my $parent = shift; + my($name, $attrs, $children) = @$element; + + my $device = $self->newDevice(%{$_[0]}); + $device->parent($parent); + for my $childElement (@$children) { + my $childName = $childElement->[0]; + + if ($childName eq 'deviceList') { + my $childDevices = $childElement->[2]; + next if (ref $childDevices ne "ARRAY"); + for my $deviceElement (@$childDevices) { + my $childDevice = $self->parseDeviceElement($deviceElement, + $device, + @_); + if ($childDevice) { + $device->addChild($childDevice); + } + } + } + elsif ($childName eq 'serviceList') { + my $services = $childElement->[2]; + for my $serviceElement (@$services) { + my $service = $self->parseServiceElement($serviceElement, + @_); + if ($service) { + $device->addService($service); + } + } + } + elsif (UPnP::Common::Device::isProperty($childName)) { + my $value = $childElement->[2]; + $device->$childName($value); + } + } + + return $device; +} + +sub parseDeviceDescription { + my $self = shift; + my $description = shift; + my ($base, $device); + + my $parser = $self->parser; + my $element = $parser->parse($description); + if (defined($element) && ref $element eq 'ARRAY') { + my($name, $attrs, $children) = @$element; + for my $child (@$children) { + my ($childName) = @$child; + if ($childName eq 'URLBase') { + $base = $child->[2]; + } + elsif ($childName eq 'device') { + $device = $self->parseDeviceElement($child, + undef, + @_); + } + } + } + + return ($device, $base); +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::Device; + +use strict; + +use Carp; +use Scalar::Util qw(weaken); + +use vars qw($AUTOLOAD %deviceProperties); +for my $prop (qw(deviceType friendlyName manufacturer + manufacturerURL modelDescription modelName + modelNumber modelURL serialNumber UDN + presentationURL UPC location)) { + $deviceProperties{$prop}++; +} + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my %args = @_; + + $self = bless {}, $class; + if ($args{Location}) { + $self->location($args{Location}); + } + + return $self; +} + +sub addChild { + my $self = shift; + my $child = shift; + + push @{$self->{_children}}, $child; +} + +sub addService { + my $self = shift; + my $service = shift; + + push @{$self->{_services}}, $service; +} + +sub parent { + my $self = shift; + + if (@_) { + $self->{_parent} = shift; + weaken($self->{_parent}); + } + + return $self->{_parent}; +} + +sub children { + my $self = shift; + + if (ref $self->{_children}) { + return @{$self->{_children}}; + } + + return (); +} + +sub services { + my $self = shift; + + if (ref $self->{_services}) { + return @{$self->{_services}}; + } + + return (); +} + +sub getService { + my $self = shift; + my $id = shift; + + for my $service ($self->services) { + if ($id && + ($id eq $service->serviceId) || + ($id eq $service->serviceType)) { + return $service; + } + } + + return undef; +} + +sub isProperty { + my $prop = shift; + return $deviceProperties{$prop}; +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + croak "invalid attribute method: ->$attr()" unless $deviceProperties{$attr}; + + $self->{uc $attr} = shift if @_; + return $self->{uc $attr}; +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::Service; + +use strict; + +use SOAP::Lite; +use Carp; + +use vars qw($AUTOLOAD %serviceProperties); +for my $prop (qw(serviceType serviceId SCPDURL controlURL + eventSubURL base)) { + $serviceProperties{$prop}++; +} + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + return bless {}, $class; +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + croak "invalid attribute method: ->$attr()" unless $serviceProperties{$attr}; + + $self->{uc $attr} = shift if @_; + return $self->{uc $attr}; +} + +sub isProperty { + my $prop = shift; + return $serviceProperties{$prop}; +} + +sub addAction { + my $self = shift; + my $action = shift; + + $self->{_actions}->{$action->name} = $action; +} + +sub addStateVariable { + my $self = shift; + my $var = shift; + + $self->{_stateVariables}->{$var->name} = $var; +} + +sub actions { + my $self = shift; + + $self->_loadDescription; + + if (defined($self->{_actions})) { + return values %{$self->{_actions}}; + } + + return (); +} + +sub getAction { + my $self = shift; + my $name = shift; + + $self->_loadDescription; + + if (defined($self->{_actions})) { + return $self->{_actions}->{$name}; + } + + return undef; +} + +sub stateVariables { + my $self = shift; + + $self->_loadDescription; + + if (defined($self->{_stateVariables})) { + return values %{$self->{_stateVariables}}; + } + + return (); +} + +sub getStateVariable { + my $self = shift; + my $name = shift; + + $self->_loadDescription; + + if (defined($self->{_stateVariables})) { + return $self->{_stateVariables}->{$name}; + } + + return undef; +} + +sub getArgumentType { + my $self = shift; + my $arg = shift; + + $self->_loadDescription; + + my $var = $self->getStateVariable($arg->relatedStateVariable); + if ($var) { + return $var->SOAPType; + } + + return undef; +} + +sub _parseArgumentList { + my $self = shift; + my $list = shift; + my $action = shift; + + return if (! ref $list); + + for my $argumentElement (@$list) { + my($name, $attrs, $children) = @$argumentElement; + if ($name eq 'argument') { + my $argument = UPnP::Common::Argument->new; + for my $argumentChild (@$children) { + my ($childName) = @$argumentChild; + if ($childName eq 'name') { + $argument->name($argumentChild->[2]); + } + elsif ($childName eq 'direction') { + my $direction = $argumentChild->[2]; + if ($direction eq 'in') { + $action->addInArgument($argument); + } + elsif ($direction eq 'out') { + $action->addOutArgument($argument); + } + } + elsif ($childName eq 'relatedStateVariable') { + $argument->relatedStateVariable($argumentChild->[2]); + } + elsif ($childName eq 'retval') { + $action->retval($argument); + } + } + } + } +} + +sub _parseActionList { + my $self = shift; + my $list = shift; + + for my $actionElement (@$list) { + my($name, $attrs, $children) = @$actionElement; + if ($name eq 'action') { + my $action = UPnP::Common::Action->new; + for my $actionChild (@$children) { + my ($childName) = @$actionChild; + if ($childName eq 'name') { + $action->name($actionChild->[2]); + } + elsif ($childName eq 'argumentList') { + $self->_parseArgumentList($actionChild->[2], + $action); + } + } + $self->addAction($action); + } + } +} + +sub _parseStateTable { + my $self = shift; + my $list = shift; + + for my $varElement (@$list) { + my($name, $attrs, $children) = @$varElement; + if ($name eq 'stateVariable') { + my $var = UPnP::Common::StateVariable->new(exists $attrs->{sendEvents} && ($attrs->{sendEvents} eq 'yes')); + for my $varChild (@$children) { + my ($childName) = @$varChild; + if ($childName eq 'name') { + $var->name($varChild->[2]); + } + elsif ($childName eq 'dataType') { + $var->type($varChild->[2]); + } + } + $self->addStateVariable($var); + } + } +} + +sub parseServiceDescription { + my $self = shift; + my $parser = shift; + my $description = shift; + + my $element = $parser->parse($description); + if (defined($element) && ref $element eq 'ARRAY') { + my($name, $attrs, $children) = @$element; + for my $child (@$children) { + my ($childName) = @$child; + if ($childName eq 'actionList') { + $self->_parseActionList($child->[2]); + } + elsif ($childName eq 'serviceStateTable') { + $self->_parseStateTable($child->[2]); + } + } + } + else { + carp("Malformed SCPD document"); + } +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::Action; + +use strict; + +use Carp; + +use vars qw($AUTOLOAD %actionProperties); +for my $prop (qw(name retval)) { + $actionProperties{$prop}++; +} + +sub new { + return bless {}, shift; +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + croak "invalid attribute method: ->$attr()" unless $actionProperties{$attr}; + + $self->{uc $attr} = shift if @_; + return $self->{uc $attr}; +} + +sub addInArgument { + my $self = shift; + my $argument = shift; + + push @{$self->{_inArguments}}, $argument; +} + +sub addOutArgument { + my $self = shift; + my $argument = shift; + + push @{$self->{_outArguments}}, $argument; +} + +sub inArguments { + my $self = shift; + + if (defined $self->{_inArguments}) { + return @{$self->{_inArguments}}; + } + + return (); +} + +sub outArguments { + my $self = shift; + + if (defined $self->{_outArguments}) { + return @{$self->{_outArguments}}; + } + + return (); +} + +sub arguments { + my $self = shift; + + return ($self->inArguments, $self->outArguments); +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::Argument; + +use strict; + +use Carp; + +use vars qw($AUTOLOAD %argumentProperties); +for my $prop (qw(name relatedStateVariable)) { + $argumentProperties{$prop}++; +} + +sub new { + return bless {}, shift; +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + croak "invalid attribute method: ->$attr()" unless $argumentProperties{$attr}; + + $self->{uc $attr} = shift if @_; + return $self->{uc $attr}; +} + +# ---------------------------------------------------------------------- + +package UPnP::Common::StateVariable; + +use strict; + +use Carp; + +use vars qw($AUTOLOAD %varProperties); +for my $prop (qw(name type evented)) { + $varProperties{$prop}++; +} + +sub new { + my $self = bless {}, shift; + $self->evented(shift); + return $self; +} + +sub SOAPType { + my $self = shift; + return UPnP::Common::UPnPToSOAPType($self->type); +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + croak "invalid attribute method: ->$attr()" unless $varProperties{$attr}; + + $self->{uc $attr} = shift if @_; + return $self->{uc $attr}; +} + + +# ---------------------------------------------------------------------- + +package UPnP::Common::Parser; + +use XML::Parser::Lite; + +# Parser code borrowed from SOAP::Lite. This package uses the +# event-driven XML::Parser::Lite parser to construct a nested data +# structure - a poor man's DOM. Each XML element in the data structure +# is represented by an array ref, with the values (listed by subscript +# below) corresponding with: +# 0 - The element name. +# 1 - A hash ref representing the element attributes. +# 2 - An array ref holding either child elements or concatenated +# character data. + +sub new { + my $class = shift; + + return bless { _parser => XML::Parser::Lite->new }, $class; +} + +sub parse { + my $self = shift; + my $parser = $self->{_parser}; + + $parser->setHandlers(Final => sub { shift; $self->final(@_) }, + Start => sub { shift; $self->start(@_) }, + End => sub { shift; $self->end(@_) }, + Char => sub { shift; $self->char(@_) },); + $parser->parse(shift); +} + +sub final { + my $self = shift; + my $parser = $self->{_parser}; + + # clean handlers, otherwise ControlPoint::Parser won't be deleted: + # it refers to XML::Parser which refers to subs from ControlPoint::Parser + undef $self->{_values}; + $parser->setHandlers(Final => undef, + Start => undef, + End => undef, + Char => undef,); + $self->{_done}; +} + +sub start { push @{shift->{_values}}, [shift, {@_}] } + +sub char { push @{shift->{_values}->[-1]->[3]}, shift } + +sub end { + my $self = shift; + my $done = pop @{$self->{_values}}; + $done->[2] = defined $done->[3] ? join('',@{$done->[3]}) : '' unless ref $done->[2]; + undef $done->[3]; + @{$self->{_values}} ? (push @{$self->{_values}->[-1]->[2]}, $done) + : ($self->{_done} = $done); +} + +1; +__END__ + +=head1 NAME + +UPnP::Common - Internal modules and methods for the UPnP +implementation. The C<UPnP::ControlPoint> and C<UPnP::DeviceManager> +modules should be used. + +=head1 DESCRIPTION + +Part of the Perl UPnP implementation suite. + +=head1 SEE ALSO + +UPnP documentation and resources can be found at L<http://www.upnp.org>. + +The C<SOAP::Lite> module can be found at L<http://www.soaplite.com>. + +UPnP implementations in other languages include the UPnP SDK for Linux +(L<http://upnp.sourceforge.net/>), Cyberlink for Java +(L<http://www.cybergarage.org/net/upnp/java/index.html>) and C++ +(L<http://sourceforge.net/projects/clinkcc/>), and the Microsoft UPnP +SDK +(L<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/upnp/upnp/universal_plug_and_play_start_page.asp>). + +=head1 AUTHOR + +Vidur Apparao (vidurapparao@users.sourceforge.net) + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2004 by Vidur Apparao + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8 or, +at your option, any later version of Perl 5 you may have available. + + +=cut + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:t +# End: diff --git a/fhem/FHEM/lib/UPnP/ControlPoint.pm b/fhem/FHEM/lib/UPnP/ControlPoint.pm new file mode 100644 index 000000000..c71b59659 --- /dev/null +++ b/fhem/FHEM/lib/UPnP/ControlPoint.pm @@ -0,0 +1,1595 @@ +package UPnP::ControlPoint; + +use 5.006; +use strict; +use warnings; + +use Carp; +use IO::Socket::INET; +use Socket; +use IO::Select; +use HTTP::Daemon; +use HTTP::Headers; +use LWP::UserAgent; +use UPnP::Common; + +use vars qw($VERSION @ISA); + +require Exporter; + +our @ISA = qw(Exporter UPnP::Common::DeviceLoader); +our $VERSION = $UPnP::Common::VERSION; + +use constant DEFAULT_SSDP_SEARCH_PORT => 8008; +use constant DEFAULT_SUBSCRIPTION_PORT => 8058; +use constant DEFAULT_SUBSCRIPTION_URL => '/eventSub'; + +our %IGNOREIP; + +sub new { + my($self, %args) = @_; + my $class = ref($self) || $self; + + $self = $class->SUPER::new(%args); + + my $searchPort = $args{SearchPort} || DEFAULT_SSDP_SEARCH_PORT; + my $subscriptionPort = $args{SubscriptionPort} || DEFAULT_SUBSCRIPTION_PORT; + my $maxWait = $args{MaxWait} || 3; + %IGNOREIP = %{$args{IgnoreIP}}; + + # Create the socket on which search requests go out + $self->{_searchSocket} = IO::Socket::INET->new(Proto => 'udp', + LocalPort => $searchPort) || + croak("Error creating search socket: $!\n"); + setsockopt($self->{_searchSocket}, + IP_LEVEL, + IP_MULTICAST_TTL, + pack 'I', 4); + $self->{_maxWait} = $maxWait; + + # Create the socket on which we'll listen for events to which we are + # subscribed. + $self->{_subscriptionSocket} = HTTP::Daemon->new( + LocalPort => $subscriptionPort, Reuse=>1, Listen=>20) || + croak("Error creating subscription socket: $!\n"); + $self->{_subscriptionURL} = $args{SubscriptionURL} || DEFAULT_SUBSCRIPTION_URL; + $self->{_subscriptionPort} = $subscriptionPort; + + # Create the socket on which we'll listen for SSDP Notifications. + $self->{_ssdpMulticastSocket} = IO::Socket::INET->new( + Proto => 'udp', + Reuse => 1, + LocalPort => SSDP_PORT) || + croak("Error creating SSDP multicast listen socket: $!\n"); + my $ip_mreq = inet_aton(SSDP_IP) . INADDR_ANY; + setsockopt($self->{_ssdpMulticastSocket}, + IP_LEVEL, + IP_ADD_MEMBERSHIP, + $ip_mreq); + setsockopt($self->{_ssdpMulticastSocket}, + IP_LEVEL, + IP_MULTICAST_TTL, + pack 'I', 4); + + return $self; +} + +sub DESTROY { + my $self = shift; + + for my $subscription (values %{$self->{_subscriptions}}) { + if ($subscription) { + $subscription->unsubscribe; + } + } +} + +sub searchByType { + my $self = shift; + my $type = shift; + my $callback = shift; + + my $search = UPnP::ControlPoint::Search->new(Callback => $callback, + Type => $type); + $self->{_activeSearches}->{$search} = $search; + $self->_startSearch($type); + return $search; +} + +sub searchByUDN { + my $self = shift; + my $udn = shift; + my $callback = shift; + + my $search = UPnP::ControlPoint::Search->new(Callback => $callback, + UDN => $udn); + $self->{_activeSearches}->{$search} = $search; + $self->_startSearch("upnp:rootdevice"); + $search; +} + +sub searchByFriendlyName { + my $self = shift; + my $name = shift; + my $callback = shift; + + my $search = UPnP::ControlPoint::Search->new(Callback => $callback, + FriendlyName => $name); + $self->{_activeSearches}->{$search} = $search; + $self->_startSearch("upnp:rootdevice"); + $search; +} + +sub stopSearch { + my $self = shift; + my $search = shift; + + delete $self->{_activeSearches}->{$search}; +} + +sub sockets { + my $self = shift; + + return ($self->{_subscriptionSocket}, + $self->{_ssdpMulticastSocket}, + $self->{_searchSocket},); +} + +sub handleOnce { + my $self = shift; + my $socket = shift; + + if ($socket == $self->{_searchSocket}) { + $self->_receiveSearchResponse($socket); + } + elsif ($socket == $self->{_ssdpMulticastSocket}) { + $self->_receiveSSDPEvent($socket); + } + elsif ($socket == $self->{_subscriptionSocket}) { + if (my $connect = $socket->accept()) { + return if ($IGNOREIP{$connect->peerhost()}); + $self->_receiveSubscriptionNotification($connect); + } + } +} + +sub handle { + my $self = shift; + my @mysockets = $self->sockets(); + my $select = IO::Select->new(@mysockets); + + $self->{_handling} = 1; + while ($self->{_handling}) { + my @sockets = $select->can_read(1); + for my $sock (@sockets) { + $self->handleOnce($sock); + } + } +} + +sub stopHandling { + my $self = shift; + $self->{_handling} = 0; +} + +sub subscriptionURL { + my $self = shift; + return URI->new_abs($self->{_subscriptionURL}, + 'http://' . UPnP::Common::getLocalIP() . ':' . + $self->{_subscriptionPort}); +} + +sub addSubscription { + my $self = shift; + my $subscription = shift; + + $self->{_subscriptions}->{$subscription->SID} = $subscription; +} + +sub removeSubscription { + my $self = shift; + my $subscription = shift; + + delete $self->{_subscriptions}->{$subscription->SID}; +} + +sub _startSearch { + my $self = shift; + my $target = shift; + + my $header = 'M-SEARCH * HTTP/1.1' . CRLF . + 'HOST: ' . SSDP_IP . ':' . SSDP_PORT . CRLF . + 'MAN: "ssdp:discover"' . CRLF . + 'ST: ' . $target . CRLF . + 'MX: ' . $self->{_maxWait} . CRLF . + CRLF; + + my $destaddr = sockaddr_in(SSDP_PORT, inet_aton(SSDP_IP)); + send($self->{_searchSocket}, $header, 0, $destaddr); +} + +sub _parseUSNHeader { + my $usn = shift; + + my ($udn, $deviceType, $serviceType); + + if ($usn =~ /^uuid:schemas(.*?):device(.*?):(.*?):(.+)$/) { + $udn = 'uuid:' . $4; + $deviceType = 'urn:schemas' . $1 . ':device' . $2 . ':' . $3; + } + elsif ($usn =~ /^uuid:(.+?)::/) { + $udn = 'uuid:' . $1; + if ($usn =~ /urn:(.+)$/) { + my $urn = $1; + if ($usn =~ /:service:/) { + $serviceType = 'urn:' . $urn; + } + elsif ($usn =~ /:device:/) { + $deviceType = 'urn:' . $urn; + } + } + } + else { + $udn = $usn; + } + + return ($udn, $deviceType, $serviceType); +} + +sub _firstLocation { + my $headers = shift; + my $location = $headers->header('Location'); + + return $location if $location; + + my $al = $headers->header('AL'); + if ($al && $al =~ /^<(\S+?)>/) { + return $1; + } + + return undef; +} + +sub newService { + my $self = shift; + + return UPnP::ControlPoint::Service->new(@_); +} + +sub newDevice { + my $self = shift; + + return UPnP::ControlPoint::Device->new(@_); +} + +sub _createDevice { + my $self = shift; + my $location = shift; + my $device; + + # We've found examples of where devices claim to do transfer + # encoding, but wind up sending chunks without chunk size headers. + # This code temporarily disables the TE header in the request. + push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, SendTE => 0); + my $ua = LWP::UserAgent->new(timeout => 20); + my $response = $ua->get($location); + + my $base; + if ($response->is_success && $response->content ne '') { + ($device, $base) = $self->parseDeviceDescription($response->content, + {Location => $location}, + {ControlPoint => $self}); + } + else { + carp("Loading device description failed with error: " . + $response->code . " " . $response->message); + } + pop(@LWP::Protocol::http::EXTRA_SOCK_OPTS); + + if ($device) { + $device->base($base ? $base : $location); + if ($response->is_success && $response->content ne '') { + $device->descriptionDocument($response->content); + } + } + + return $device; +} + +sub _getDeviceFromHeaders { + my $self = shift; + my $headers = shift; + my $create = shift; + + my $location = _firstLocation($headers); + my ($udn, $deviceType, $serviceType) = + _parseUSNHeader($headers->header('USN')); + my $device = $self->{_devices}->{$udn}; + if (!defined($device) && $create) { + $device = $self->_createDevice($location); + if ($device) { + $self->{_devices}->{$udn} = $device; + } + } + + return $device; +} + +sub _deviceAdded { + my $self = shift; + my $device = shift; + + for my $search (values %{$self->{_activeSearches}}) { + $search->deviceAdded($device); + } +} + +sub _deviceRemoved { + my $self = shift; + my $device = shift; + + for my $search (values %{$self->{_activeSearches}}) { + $search->deviceRemoved($device); + } +} + + use Data::Dumper; +sub _receiveSearchResponse { + my $self = shift; + my $socket = shift; + my $buf = ''; + + my $peer = recv($socket, $buf, 2048, 0); + my @peerdata = unpack_sockaddr_in($peer); + return if ($IGNOREIP{inet_ntoa($peerdata[1])}); + + if ($buf !~ /\015?\012\015?\012/) { + return; + } + + $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines + unless ($buf =~ s/^(\S+)[ \t]+(\S+)[ \t]+(\S+)[^\012]*\012//) { + # Bad header + return; + } + + # Basic check to see if the response is actually for a search + my $found = 0; + foreach my $searchkey (keys %{$self->{_activeSearches}}) { + my $search = $self->{_activeSearches}->{$searchkey}; + if ($search->{_type} && $buf =~ $search->{_type}) { + $found = 1; + last; + } + + if ($search->{_udn} && $buf =~ $search->{_udn}) { + $found = 1; + last; + } + + if ($search->{_friendlyName} && $search->{_friendlyName}) { + $found = 1; + last; + } + + } + + if (! $found) { + #print "Unknown response: " . Dumper($buf); #ALW uncomment + return; + } + + my $code = $2; + if ($code ne '200') { + # We expect a success response code + return; + } + + my $headers = UPnP::Common::parseHTTPHeaders($buf); + my $device = $self->_getDeviceFromHeaders($headers, 1); + if ($device) { + $self->_deviceAdded($device); + } +} + +sub _receiveSSDPEvent { + my $self = shift; + my $socket = shift; + my $buf = ''; + + my $peer = recv($socket, $buf, 2048, 0); + my @peerdata = unpack_sockaddr_in($peer); + return if ($IGNOREIP{inet_ntoa($peerdata[1])}); + + if ($buf !~ /\015?\012\015?\012/) { + return; + } + + + $buf =~ s/^(?:\015?\012)+//; # ignore leading blank lines + unless ($buf =~ s/^(\S+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012//) { + # Bad header + return; + } + + #print Dumper($buf); #ALW uncomment + + my $method = $1; + if ($method ne 'NOTIFY') { + # We only care about notifications + return; + } + + my $headers = UPnP::Common::parseHTTPHeaders($buf); + my $eventType = $headers->header('NTS'); + my $device = $self->_getDeviceFromHeaders($headers, + $eventType =~ /alive/ ? + 1 : 0); + + if ($device) { + if ($eventType =~ /alive/) { + $self->_deviceAdded($device); + } + elsif ($eventType =~ /byebye/) { + $self->_deviceRemoved($device); + $self->{_devices}->{$device->UDN()} = undef; + } + } +} + +sub _parseProperty { + my $self = shift; + my $element = shift; + my ($name, $attrs, $children) = @$element; + my ($key, $value); + + if ($name =~ /property/) { + my $childElement = $children->[0]; + $key = $childElement->[0]; + $value = $childElement->[2]; + } + + ($key, $value); +} + + +sub _parsePropertySet { + my $self = shift; + my $content = shift; + my %properties = (); + + my $parser = $self->parser; + my $element = $parser->parse($content); + if (defined($element) && (ref $element eq 'ARRAY') && + $element->[0] =~ /propertyset/) { + my($name, $attrs, $children) = @$element; + for my $child (@$children) { + my ($key, $value) = $self->_parseProperty($child); + if ($key) { + $properties{$key} = $value; + } + } + } + + return %properties; +} + +sub _receiveSubscriptionNotification { + my $self = shift; + my $connect = shift; + + my $request = $connect->get_request(); + if ($request && ($request->method eq 'NOTIFY') && + ($request->header('NT') eq 'upnp:event') && + ($request->header('NTS') eq 'upnp:propchange')) { + my $sid = $request->header('SID'); + my $subscription = $self->{_subscriptions}->{$sid}; + if ($subscription) { + my %propSet = $self->_parsePropertySet($request->content); + $subscription->propChange(%propSet); + } + } + + $connect->send_response(HTTP::Response->new(HTTP::Status::RC_OK)); + $connect->close; +} + + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::Device; + +use strict; + +use vars qw(@ISA); + +use UPnP::Common; + +our @ISA = qw(UPnP::Common::Device); + +sub base { + my $self = shift; + my $base = shift; + + if (defined($base)) { + $self->{_base} = $base; + + for my $service ($self->services) { + $service->base($base); + } + + for my $device ($self->children) { + $device->base($base); + } + } + + return $self->{_base}; +} + +sub descriptionDocument { + my $self = shift; + my $descriptionDocument = shift; + + if (defined($descriptionDocument)) { + $self->{_descriptionDocument} = $descriptionDocument; + } + + return $self->{_descriptionDocument}; +} + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::Service; + +use strict; + +use Socket; +use Scalar::Util qw(weaken); +use SOAP::Lite; +use Carp; + +use vars qw($AUTOLOAD @ISA %urlProperties); + +use UPnP::Common; + +our @ISA = qw(UPnP::Common::Service); + +for my $prop (qw(SCPDURL controlURL eventSubURL)) { + $urlProperties{$prop}++; +} + +sub new { + my ($self, %args) = @_; + my $class = ref($self) || $self; + + $self = $class->SUPER::new(%args); + if ($args{ControlPoint}) { + $self->{_controlPoint} = $args{ControlPoint}; + weaken($self->{_controlPoint}); + } + + return $self; +} + +sub AUTOLOAD { + my $self = shift; + my $attr = $AUTOLOAD; + $attr =~ s/.*:://; + return if $attr eq 'DESTROY'; + + my $superior = "SUPER::$attr"; + my $val = $self->$superior(@_); + if ($urlProperties{$attr}) { + my $base = $self->base; + if ($base) { + return URI->new_abs($val, $base); + } + + return URI->new($val); + } + + return $val; +} + +sub controlProxy { + my $self = shift; + + $self->_loadDescription; + + return UPnP::ControlPoint::ControlProxy->new($self); +} + +sub queryStateVariable { + my $self = shift; + my $name = shift; + + $self->_loadDescription; + + my $var = $self->getStateVariable($name); + if (!$var) { croak("No such state variable $name"); } + if (!$var->evented) { croak("Variable $name is not evented"); } + + my $result; + if ($SOAP::Lite::VERSION >= 0.67) { + $result = SOAP::Lite + ->ns("u") + ->uri('urn:schemas-upnp-org:control-1-0') + ->proxy($self->controlURL) + ->call('QueryStateVariable' => + SOAP::Data->name('varName') + ->uri('urn:schemas-upnp-org:control-1-0') + ->value($name)); + } else { + $result = SOAP::Lite + ->uri('urn:schemas-upnp-org:control-1-0') + ->proxy($self->controlURL) + ->call('QueryStateVariable' => + SOAP::Data->name('varName') + ->uri('urn:schemas-upnp-org:control-1-0') + ->value($name)); + } + + if ($result->fault()) { + carp("Query failed with fault " . $result->faultstring()); + return undef; + } + + return $result->result; +} + +sub subscribe { + my $self = shift; + my $callback = shift; + my $timeout = shift; + my $cp = $self->{_controlPoint}; + + if (!defined $UPnP::Common::LocalIP) { + # Find our local IP + my $u = URI->new($self->eventSubURL); + my $proto = getprotobyname('tcp'); + socket(Socket_Handle, PF_INET, SOCK_STREAM, $proto); + my $sin = sockaddr_in($u->port(),inet_aton($u->host())); + connect(Socket_Handle,$sin); + + my ($port, $addr) = sockaddr_in(getsockname(Socket_Handle)); + close(Socket_Handle); + UPnP::Common::setLocalIP($addr); + } + + if (defined($cp)) { + my $url = $self->eventSubURL; + my $request = HTTP::Request->new('SUBSCRIBE', + "$url"); + $request->header('NT', 'upnp:event'); + $request->header('Callback', '<' . $cp->subscriptionURL . '>'); + $request->header('Timeout', + 'Second-' . defined($timeout) ? $timeout : 'infinite'); + my $ua = LWP::UserAgent->new(timeout => 20); + my $response = $ua->request($request); + + if ($response->is_success && + $response->code == 200) { + my $sid = $response->header('SID'); + $timeout = $response->header('Timeout'); + if ($timeout =~ /^Second-(\d+)$/) { + $timeout = $1; + } + + my $subscription = UPnP::ControlPoint::Subscription->new( + Service => $self, + Callback => $callback, + SID => $sid, + Timeout => $timeout, + EventSubURL => "$url"); + $cp->addSubscription($subscription); + return $subscription; + } + else { + carp("Subscription request failed with error: " . + $response->code . " " . $response->message); + } + } + + return undef; +} + +sub unsubscribe { + my $self = shift; + my $subscription = shift; + + my $url = $self->eventSubURL; + my $request = HTTP::Request->new('UNSUBSCRIBE', + "$url"); + $request->header('SID', $subscription->SID); + my $ua = LWP::UserAgent->new(timeout => 20); + my $response = $ua->request($request); + + if ($response->is_success) { + my $cp = $self->{_controlPoint}; + + if (defined($cp)) { + $cp->removeSubscription($subscription); + } + } + else { + if ($response->code != 412) { + carp("Unsubscription request failed with error: " . + $response->code . " " . $response->message); + } + } +} + +sub _loadDescription { + my $self = shift; + + if ($self->{_loadedDescription}) { + return; + } + + my $location = $self->SCPDURL; + my $cp = $self->{_controlPoint}; + unless (defined($location)) { + carp("Service doesn't have a SCPD location"); + return; + } + unless (defined($cp)) { + carp("ControlPoint instance no longer exists"); + return; + } + my $parser = $cp->parser; + + push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, SendTE => 0); + my $ua = LWP::UserAgent->new(timeout => 20); + my $response = $ua->get($location); + + if ($response->is_success) { + $self->parseServiceDescription($parser, $response->content); + } + else { + carp("Error loading SCPD document: $!"); + } + + pop(@LWP::Protocol::http::EXTRA_SOCK_OPTS); + + $self->{_loadedDescription} = 1; +} + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::ControlProxy; + +use strict; + +use SOAP::Lite; +use Carp; + +use vars qw($AUTOLOAD); + + +sub new { + my($class, $service) = @_; + + if ($SOAP::Lite::VERSION >= 0.67) { + return bless { + _service => $service, + _proxy => SOAP::Lite->ns("u")->uri($service->serviceType)->proxy($service->controlURL), + }, $class; + } else { + return bless { + _service => $service, + _proxy => SOAP::Lite->uri($service->serviceType)->proxy($service->controlURL), + }, $class; + } +} + +sub AUTOLOAD { + my $self = shift; + my $service = $self->{_service}; + my $proxy = $self->{_proxy}; + my $method = $AUTOLOAD; + $method =~ s/.*:://; + return if $method eq 'DESTROY'; + + my $action = $service->getAction($method); + croak "invalid method: ->$method()" unless $action; + + my @inArgs; + for my $arg ($action->inArguments) { + my $val = shift; + my $type = $service->getArgumentType($arg); + push @inArgs, SOAP::Data->type($type => $val)->name($arg->name); + } + return UPnP::ControlPoint::ActionResult->new( + Action => $action, + Service => $service, + SOM => $proxy->call($method => @inArgs)); +} + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::ActionResult; + +use strict; + +use SOAP::Lite; +use HTML::Entities (); +use Carp; + +use vars qw($AUTOLOAD); + +sub new { + my($class, %args) = @_; + my $som = $args{SOM}; + + my $self = bless { + _som => $som, + }, $class; + + unless (defined($som->fault())) { + for my $out ($args{Action}->outArguments) { + my $name = $out->name; + my $data = $som->match('/Envelope/Body//' . $name)->dataof(); + if ($data) { + my $type = $args{Service}->getArgumentType($out); + $data->type($type); + if ($type eq 'string') { + $self->{_results}->{$name} = HTML::Entities::decode( + $data->value); + } + else { + $self->{_results}->{$name} = $data->value; + } + } + } + } + + return $self; +} + +sub isSuccessful { + my $self = shift; + + return !defined($self->{_som}->fault()); +} + +sub getValue { + my $self = shift; + my $name = shift; + + if (defined($self->{_results})) { + return $self->{_results}->{$name}; + } + + return undef; +} + +sub AUTOLOAD { + my $self = shift; + my $method = $AUTOLOAD; + $method =~ s/.*:://; + return if $method eq 'DESTROY'; + + return $self->{_som}->$method(@_); +} + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::Search; + +use strict; + +sub new { + my($class, %args) = @_; + + return bless { + _callback => $args{Callback}, + _type => $args{Type}, + _udn => $args{UDN}, + _friendlyName => $args{FriendlyName}, + }, $class; +} + +sub _passesFilter { + my $self = shift; + my $device = shift; + + my $type = $self->{_type}; + my $name = $self->{_friendlyName}; + my $udn = $self->{_udn}; + + if ((!defined($type) || ($type eq $device->deviceType()) || + ($type eq 'ssdp:all')) && + (!defined($name) || ($name eq $device->friendlyName())) && + (!defined($udn) || ($udn eq $device->udn()))) { + return 1; + } + + return 0; +} + +sub deviceAdded { + my $self = shift; + my $device = shift; + + if ($self->_passesFilter($device) && + !$self->{_devices}->{$device}) { + &{$self->{_callback}}($self, $device, 'deviceAdded'); + $self->{_devices}->{$device}++; + } +} + +sub deviceRemoved { + my $self = shift; + my $device = shift; + + if ($self->_passesFilter($device) && + $self->{_devices}->{$device}) { + &{$self->{_callback}}($self, $device, 'deviceRemoved'); + delete $self->{_devices}->{$device}; + } +} + +# ---------------------------------------------------------------------- + +package UPnP::ControlPoint::Subscription; + +use strict; + +use Time::HiRes; +use Scalar::Util qw(weaken); +use Carp; + +sub new { + my($class, %args) = @_; + + my $self = bless { + _callback => $args{Callback}, + _sid => $args{SID}, + _timeout => $args{Timeout}, + _startTime => Time::HiRes::time(), + _eventSubURL => $args{EventSubURL}, + }, $class; + weaken($self->{_service} = $args{Service}); + + return $self; +} + +sub SID { + my $self = shift; + + return $self->{_sid}; +} + +sub timeout { + my $self = shift; + + return $self->{_timeout}; +} + +sub expired { + my $self = shift; + + if ($self->{_timeout} eq 'INFINITE') { + return 0; + } + + my $now = Time::HiRes::time(); + if ($now - $self->{_startTime} > $self->{_timeout}) { + return 1; + } + + return 0; +} + +sub renew { + my $self = shift; + my $timeout = shift; + + my $url = $self->{_eventSubURL}; + my $request = HTTP::Request->new('SUBSCRIBE', + "$url"); + $request->header('SID', $self->{_sid}); + $request->header('Timeout', + 'Second-' . defined($timeout) ? $timeout : 'infinite'); + + my $ua = LWP::UserAgent->new(timeout => 20); + my $response = $ua->request($request); + + if ($response->is_success) { + $timeout = $response->header('Timeout'); + if ($timeout =~ /^Second-(\d+)$/) { + $timeout = $1; + } + + $self->{_timeout} = $timeout; + $self->{_startTime} = Time::HiRes::time(); + } + else { + carp("Renewal of subscription failed with error: " . + $response->code . " " . $response->message); + } + + return $self; +} + +sub unsubscribe { + my $self = shift; + + if ($self->{_service}) { + $self->{_service}->unsubscribe($self); + } +} + +sub propChange { + my $self = shift; + my %properties = @_; + + if ($self->{_service}) { + &{$self->{_callback}}($self->{_service}, %properties); + } +} + +1; +__END__ + +=head1 NAME + +UPnP::ControlPoint - A UPnP ControlPoint implementation. + +=head1 SYNOPSIS + + use UPnP::ControlPoint; + + my $cp = UPnP::ControlPoint->new; + my $search = $cp->searchByType("urn:schemas-upnp-org:device:TestDevice:1", + \&callback); + $cp->handle; + + sub callback { + my ($search, $device, $action) = @_; + + if ($action eq 'deviceAdded') { + print("Device: " . $device->friendlyName . " added. Device contains:\n"); + for my $service ($device->services) { + print("\tService: " . $service->serviceType . "\n"); + } + } + elsif ($action eq 'deviceRemoved') { + print("Device: " . $device->friendlyName . " removed\n"); + } + } + +=head1 DESCRIPTION + +Implements a UPnP ControlPoint. This module implements the various +aspects of the UPnP architecture from the standpoint of a ControlPoint: + +=over 4 + +=item 1. Discovery + +A ControlPoint can be used to actively search for devices and services +on a local network or listen for announcements as devices enter and +leave the network. The protocol used for discovery is the Simple +Service Discovery Protocol (SSDP). + +=item 2. Description + +A ControlPoint can get information describing devices and +services. Devices can be queried for services and vendor-specific +information. Services can be queried for actions and state variables. + +=item 3. Control + +A ControlPoint can invoke actions on services and poll for state +variable values. Control-related calls are generally made using the +Simple Object Access Protocol (SOAP). + +=item 4. Eventing + +ControlPoints can listen for events describing state changes in +devices and services. Subscription requests and state change events +are generally sent using the General Event Notification Architecture +(GENA). + +=back + +Since the UPnP architecture leverages several existing protocols such +as TCP, UDP, HTTP and SOAP, this module requires several Perl modules +that implement these protocols. These include +L<IO::Socket::INET|IO::Socket::INET>, +L<LWP::UserAgent|LWP::UserAgent>, +L<HTTP::Daemon|HTTP::Daemon> and +C<SOAP::Lite> (L<http://www.soaplite.com>). + +=head1 METHODS + +=head2 UPnP::ControlPoint + +A ControlPoint implementor will generally create a single instance of +the C<UPnP::ControlPoint> class (though more than one can exist within +a process assuming that they have been set up to avoid port +conflicts). + +=over 4 + +=item new ( [ARGS] ) + +Creates a C<UPnP::ControlPoint> object. Accepts the following +key-value pairs as optional arguments (default values are listed +below): + + + SearchPort Port on which search requests are received 8008 + SubscriptionPort Port on which event notification are received 8058 + SubscriptionURL URL on which event notification are received /eventSub + MaxWait Max wait before search responses should be sent 3 + +While this call creates the sockets necessary for the ControlPoint to +function, the ControlPoint is not active until its sockets are +actually serviced, either by invoking the C<handle> +method or by externally selecting using the ControlPoint's +C<sockets> and invoking the +C<handleOnce> method as each becomes ready for +reading. + +=item sockets + +Returns a list of sockets that need to be serviced for the +ControlPoint to correctly function. This method is generally used in +conjunction with the C<handleOnce> method by users who want to run +their own C<select> loop. This list of sockets should be selected for +reading and C<handleOnce> is invoked for each socket as it beoms ready +for reading. + +=item handleOnce ( SOCKET ) + +Handles the function of reading from a ControlPoint socket when it is +ready (as indicated by a C<select>). This method is used by developers +who want to run their own C<select> loop. + +=item handle + +Takes over handling of all ControlPoint sockets. Runs its own +C<select> loop, handling individual sockets as they become available +for reading. Returns only when a call to +C<stopHandling> is made (generally from a +ControlPoint callback or a signal handler). This method is an +alternative to using the C<sockets> and +C<handleOnce> methods. + +=item stopHandling + +Ends the C<select> loop run by C<handle>. This method is generally +invoked from a ControlPoint callback or a signal handler. + +=item searchByType ( TYPE, CALLBACK ) + +Used to start a search for devices on the local network by device or +service type. The C<TYPE> parameter is a string inidicating a device +or service type. Specifically, it is the string that will be put into +the C<ST> header of the SSDP C<M-SEARCH> request that is sent out. The +C<CALLBACK> parameter is a code reference to a callback that is +invoked when a device matching the search criterion is found (or a +SSDP announcement is received that such a device is entering or +leaving the network). This method returns a +L<C<UPnP::ControlPoint::Search>|/UPnP::ControlPoint::Search> object. + +The arguments to the C<CALLBACK> are the search object, the device +that has been found or newly added to or removed from the network, and +an action string which is one of 'deviceAdded' or 'deviceRemoved'. The +callback is invoked separately for each device that matches the search +criterion. + + + sub callback { + my ($search, $device, $action) = @_; + + if ($action eq 'deviceAdded') { + print("Device: " . $device->friendlyName . " added.\n"); + } + elsif ($action eq 'deviceRemoved') { + print("Device: " . $device->friendlyName . " removed\n"); + } + } + + +=item searchByUDN ( UDN, CALLBACK ) + +Used to start a search for devices on the local network by Unique +Device Name (UDN). Similar to C<searchByType>, this method sends +out a SSDP C<M-SEARCH> request with a C<ST> header of +C<upnp:rootdevice>. All responses to the search (and subsequent SSDP +announcements to the network) are filtered by the C<UDN> parameter +before resulting in C<CALLBACK> invocation. The parameters to the +callback are the same as described in C<searchByType>. + +=item searchByFriendlyName ( NAME, CALLBACK ) + +Used to start a search for devices on the local network by device +friendy name. Similar to C<searchByType>, this method sends out a +SSDP C<M-SEARCH> request with a C<ST> header of +C<upnp:rootdevice>. All responses to the search (and subsequent SSDP +announcements to the network) are filtered by the C<NAME> parameter +before resulting in C<CALLBACK> invocation. The parameters to the +callback are the same as described in C<searchByType>. + +=item stopSearch ( SEARCH ) + +The C<SEARCH> parameter is a +L<C<UPnP::ControlPoint::Search>|/UPnP::ControlPoint::Search> object +returned by one of the search methods. This method stops forwarding +SSDP events that match the search criteria of the specified search. + +=back + +=head2 UPnP::ControlPoint::Device + +A C<UPnP::ControlPoint::Device> is generally obtained using one of the +L<C<UPnP::ControlPoint>|/UPnP::ControlPoint> search methods and should +not be directly instantiated. + +=over 4 + +=item deviceType + +=item friendlyName + +=item manufacturer + +=item manufacturerURL + +=item modelDescription + +=item modelName + +=item modelNumber + +=item modelURL + +=item serialNumber + +=item UDN + +=item presentationURL + +=item UPC + +Properties received from the device's description document. The +returned values are all strings. + +=item location + +A URI representing the location of the device on the network. + +=item parent + +The parent device of this device. The value C<undef> if this device +is a root device. + +=item children + +A list of child devices. The empty list if the device has no +children. + +=item services + +A list of L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> +objects corresponding to the services implemented by this device. + +=item getService ( ID ) + +If the device implements a service whose serviceType or serviceId is +equal to the C<ID> parameter, the corresponding +L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object +is returned. Otherwise returns C<undef>. + +=back + +=head2 UPnP::ControlPoint::Service + +A C<UPnP::ControlPoint::Service> is generally obtained from a +L<C<UPnP::ControlPoint::Device>|/UPnP::ControlPoint::Device> object +using the C<services> or C<getServiceById> methods. This class should +not be directly instantiated. + +=over 4 + +=item serviceType + +=item serviceId + +=item SCPDURL + +=item controlURL + +=item eventSubURL + +Properties corresponding to the service received from the containing +device's description document. The returned values are all strings +except for the URL properties, which are absolute URIs. + +=item actions + +A list of L<C<UPnP::Common::Action>|/UPnP::Common::Action> +objects corresponding to the actions implemented by this service. + +=item getAction ( NAME ) + +Returns the +L<C<UPnP::Common::Action>|/UPnP::Common::Action> object +corresponding to the action specified by the C<NAME> parameter. +Returns C<undef> if no such action exists. + +=item stateVariables + +A list of +L<C<UPnP::Common::StateVariable>|/UPnP::Common::StateVariable> +objects corresponding to the state variables implemented by this +service. + +=item getStateVariable ( NAME ) + +Returns the +L<C<UPnP::Common::StateVariable>|/UPnP::Common::StateVariable> +object corresponding to the state variable specified by the C<NAME> +parameter. Returns C<undef> if no such state variable exists. + +=item controlProxy + +Returns a +L<C<UPnP::ControlPoint::ControlProxy>|/UPnP::ControlPoint::ControlProxy> +object that can be used to invoke actions on the service. + +=item queryStateVariable ( NAME ) + +Generates a SOAP call to the remote service to query the value of the +state variable specified by C<NAME>. Returns the value of the +variable. Returns C<undef> if no such state variable exists or the +variable is not evented. + +=item subscribe ( CALLBACK ) + +Registers an event subscription with the remote service. The code +reference specied by the C<CALLBACK> parameter is invoked when GENA +events are received from the service. This call returns a +L<C<UPnP::ControlPoint::Subscription>|/UPnP::ControlPoint::Subscription> +object corresponding to the subscription. The subscription can later +be canceled using the C<unsubscribe> method. The parameters to the +callback are the service object and a list of name-value pairs for all +of the state variables whose values are included in the corresponding +GENA event: + + sub eventCallback { + my ($service, %properties) = @_; + + print("Event received for service " . $service->serviceId . "\n"); + while (my ($key, $val) = each %properties) { + print("\tProperty ${key}'s value is " . $val . "\n"); + } + } + + +=item unsubscribe ( SUBSCRIPTION ) + +Unsubscribe from a service. This method takes the +L</UPnP::ControlPoint::Subscription> +object returned from a previous call to C<subscribe>. This method +is equivalent to calling the C<unsubscribe> method on the subscription +object itself and is included for symmetry and convenience. + +=back + +=head2 UPnP::Common::Action + +A C<UPnP::Common::Action> is generally obtained from a +L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object +using its C<actions> or C<getAction> methods. It corresponds to an +action implemented by the service. Action information is retrieved +from the service's description document. This class should not be +directly instantiated. + +=over 4 + +=item name + +The name of the action returned as a string. + +=item retval + +A L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> object that +corresponds to the action argument that is specified in the service +description document as the return value for this action. Returns +C<undef> if there is no specified return value. + +=item arguments + +A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects +corresponding to the arguments of the action. + +=item inArguments + +A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects +corresponding to the input arguments of the action. + +=item outArguments + +A list of L<C<UPnP::Common::Argument>|/UPnP::Common::Argument> objects +corresponding to the output arguments of the action. + +=back + +=head2 UPnP::Common::Argument + +A C<UPnP::Common::Argument> is generally obtained from a +L<C<UPnP::Common::Action>|/UPnP::Common::Action> object using its +C<arguments>, C<inArguments> or C<outArguments> methods. An instance +of this class corresponds to an argument of a service action, as +specified in the service's description document. This class should not +be directly instantiated. + +=over 4 + +=item name + +The name of the argument returned as a string. + +=item relatedStateVariable + +The name of the related state variable (which can be used to find the +type of the argument) returned as a string. + +=back + +=head2 UPnP::Common::StateVariable + +A C<UPnP::Common::StateVariable> is generally obtained from a +L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> object +using its C<stateVariables> or C<getStateVariable> methods. It +corresponds to a state variable implemented by the service. State +variable information is retrieved from the service's description +document. This class should not be directly instantiated. + +=over 4 + +=item name + +The name of the state variable returned as a string. + +=item evented + +Whether the state variable is evented or not. + +=item type + +The listed UPnP type of the state variable returned as a string. + +=item SOAPType + +The corresponding SOAP type of the state variable returned as a +string. + +=back + +=head2 UPnP::ControlPoint::ControlProxy + +A proxy that can be used to invoke actions on a UPnP service. An +instance of this class is generally obtained from the C<controlProxy> +method of the corresponding +L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> +object. This class should not be directly instantiated. + +An instance of this class is a wrapper on a C<SOAP::Lite> proxy. An +action is invoked as if it were a method of the proxy +object. Parameters to the action should be passed to the method. They +will automatically be coerced to the correct type. For example, to +invoke the C<Browse> method on a UPnP ContentDirectory service to get +the children of the root directory, one would say: + + + my $proxy = $service->controlProxy; + my $result = $proxy->Browse('0', 'BrowseDirectChildren', '*', 0, 0, ""); + +The result of a action invocation is an instance of the +L<C<UPnP::ControlPoint::ActionResult>|/UPnP::ControlPoint::ActionResult> +class. + +=head2 UPnP::ControlPoint::ActionResult + +An instance of this class is returned from an action invocation made +through a +L<C<UPnP::ControlPoint::ControlProxy>|/UPnP::ControlPoint::ControlProxy> +object. It is a loose wrapper on the C<SOAP::SOM> object returned from +the call made through the C<SOAP::Lite> module. All methods not +recognized by this class will be forwarded directly to the +C<SOAP::SOM> class. This class should not be directly instantiated. + +=over 4 + +=item isSuccessful + +Was the invocation successful or did it result in a fault. + +=item getValue ( NAME ) + +Gets the value of an out argument of the action invocation. The +C<NAME> parameter specifies which out argument value should be +returned. The type of the returned value depends on the type +specified in the service description file. + +=back + +=head2 UPnP::ControlPoint::Search + +A C<UPnP::ControlPoint::Search> object is returned from any successful +calls to the L<C<UPnP::ControlPoint>|/UPnP::ControlPoint> search +methods. It has no methods of its own, but can be used as a token to +pass to any subsequent C<stopSearch> calls. This class should not be +directly instantiated. + +=head2 UPnP::ControlPoint::Subscription + +A C<UPnP::ControlPoint::Search> object is returned from any successful +calls to the +L<C<UPnP::ControlPoint::Service>|/UPnP::ControlPoint::Service> +C<subscribe> method. This class should not be directly instantiated. + +=over 4 + +=item SID + +The subscription ID returned from the remote service, returned as a +string. + +=item timeout + +The timeout value returned from the remote service, returned as a +number. + +=item expired + +Has the subscription expired yet? + +=item renew + +Renews a subscription with the remote service by sending a GENA +subscription event. + +=item unsubscribe + +Unsubscribes from the remote service by sending a GENA unsubscription +event. + +=back + +=head1 SEE ALSO + +UPnP documentation and resources can be found at L<http://www.upnp.org>. + +The C<SOAP::Lite> module can be found at L<http://www.soaplite.com>. + +UPnP ControlPoint implementations in other languages include the UPnP +SDK for Linux (L<http://upnp.sourceforge.net/>), Cyberlink for Java +(L<http://www.cybergarage.org/net/upnp/java/index.html>) and C++ +(L<http://sourceforge.net/projects/clinkcc/>), and the Microsoft UPnP +SDK +(L<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/upnp/upnp/universal_plug_and_play_start_page.asp>). + +=head1 AUTHOR + +Vidur Apparao (vidurapparao@users.sourceforge.net) + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2004 by Vidur Apparao + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8 or, +at your option, any later version of Perl 5 you may have available. + +=cut + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:t +# End: diff --git a/fhem/FHEM/lib/UPnP/sonos_empty.jpg b/fhem/FHEM/lib/UPnP/sonos_empty.jpg new file mode 100644 index 000000000..01f550d53 Binary files /dev/null and b/fhem/FHEM/lib/UPnP/sonos_empty.jpg differ diff --git a/fhem/FHEM/lib/UPnP/sonos_input_default.jpg b/fhem/FHEM/lib/UPnP/sonos_input_default.jpg new file mode 100644 index 000000000..b99acc18c Binary files /dev/null and b/fhem/FHEM/lib/UPnP/sonos_input_default.jpg differ diff --git a/fhem/FHEM/lib/UPnP/sonos_input_tv.jpg b/fhem/FHEM/lib/UPnP/sonos_input_tv.jpg new file mode 100644 index 000000000..84557df6a Binary files /dev/null and b/fhem/FHEM/lib/UPnP/sonos_input_tv.jpg differ diff --git a/fhem/FHEM/lib/UPnP/sonos_playlist.jpg b/fhem/FHEM/lib/UPnP/sonos_playlist.jpg new file mode 100644 index 000000000..0ab496208 Binary files /dev/null and b/fhem/FHEM/lib/UPnP/sonos_playlist.jpg differ diff --git a/fhem/HISTORY b/fhem/HISTORY index ddb3fd8c4..966712a91 100644 --- a/fhem/HISTORY +++ b/fhem/HISTORY @@ -1,601 +1,604 @@ -- Rudi, Thu Feb 1 13:27:15 MET 2007 - Created the file HISTORY and the file README.DEV - -- Pest, Thu Feb 1 20:45 MET 2007 - Added description for attribute , - -- Rudi, Sun Feb 11 18:56:05 MET 2007 - - showtime added for pgm2 (useful for FS20 piri display) - - defattr command added, it makes easier to assign room, type, etc for a group - of devices. - - em1010.pl added to the contrib directory. It seems that we are able - to read out the EM1010PC. Not verified with multiple devices. - -- Pest, Thu Feb 11 23:35 MET 2007 - - Added doc/linux.html (multiple USDB devices, udev links) - - Linked fhem.html and commandref.html to linux.html - -- Martin Haas, Fri Feb 23 10:18 MET 2007 - - ARM-Section (NSLU2) added to doc/linux.html - -- Pest, Sat Feb 24 18:30 MET 2007 - - doc/linux.html: Module build re-written. - -- Rudi, Sun Mar 4 11:18:10 MET 2007 - Reorganization. Goal: making attribute adding/deleting more uniform - (, - possible (i.e. saving the configfile, list of possible devices etc). - - Internal changes: - - %logmods,%devmods moved to %modules. Makes things more uniform - - %logs merged into %defs - - local state info (%readings) changed to global ($defs{$d}{READINGS}) - -> No need for the listfn function in each module - -> User written scripts can more easily analyze device states - - User visible changes: - - at/notify , - modules. Now it is possible - - to have a further , - (notify & filelog use the same interface) - - to have more than one notify for the same event - - to delete at commands without strange escapes. - The delete syntax changed (no more def/at/ntfy needed) - - at/notify can have attributes - Drawback: each at and notify must have a name, which is strange first. - - logfile/modpath/pidfile/port/verbose , - Dumping and extending these attributes is easier, no special handling - required in the web-frontend. - - savefile renamed to , - - configfile global attribute added. - - save command added, it writes the statefile and then the configfile. - - delattr added to delete single attributes - - list/xmllist format changed, they contain more information. - - , - in the same format. This data is contained in the xmllist. - - disable attribute for at/notify/filelog - - rename added - -- Rudi, Tue Mar 27 20:43:15 MEST 2007 - fhemweb.pl (webpgm2) changes: - - adopted to the new syntax - - better commandline support (return <pre> formatted) - - FileLog attribute logtype added, and 4 logtypes (== gnuplot files) - defined: fs20, fht, ks300_1, ks300_2 - - links in the commandref.html file added - - device dependent attribute and set support - -- Pest, Sun Apr 08 17:55:15 MEST 2007 - em1010.pl: - - Make difference between sensors 1..4 and 5.. - - Checked values for sensor 5 (cur.energy + cur.power) - ok - - Checked values for sensor 5 cur.energy is ok, cur.power still off. - Correction factor needs to be determined. - - setTime: Without argument, now the current time of the PC is used. - - setRperKW: Factor of 10 required, 75 U/kWh -> 750. - -- Pest, Tue Apr 10 20:31:22 MEST 2007 - em1010.pl: - - Introduced new double-word function (dw) - - getDevStatus: energy values kWh/h, kWh/d, total. - -- Rudi Sat Apr 14 10:34:36 MEST 2007 - final documentations, release 4.0. Tagged as FHEM_4_0 - -- Pest, Sat Apr 14 14:21:00 MEST 2007 - - doc: linux.html (private udev-rules, not 50-..., ATTRS) - -- Pest, Sun Apr 15 14:54:30 MEST 2007 - - doc: fhem.pl and commandref.html (notifyon -> notify, correction of examples) - -- Rudi, Tue Apr 24 08:10:43 MEST 2007 - - feature: modify command added. It helps change e.g. only the time component - for an at command, without deleting and creating it again and then - reapplying all the attributes. - - feature: the , - instead. The - is used to separate ranges in the set command. - -- Rudi, Sun May 27 12:51:52 MEST 2007 - - Archiving FileLogs. Added fhemweb.pl (pgm2) code, to show logs from the - archive directory. See the attributes archivedir/archivecmd. - - Added EM1010PC suppoort (right now only with EM1000WZ). Support added - for displaying logs in the fhemweb.pl (webfrontends/pgm2) - -- Pest, Mon May 28 19:39:22 MEST 2007 - - Added 62_EMEM.pm to support EM1000-EM devices. - - doc: Update of commandref.htm (typos and EMEM). - -- Pest, Mon May 29 00:07:00 MEST 2007 - - check-in changes of 60_EM.pm to make EMEM work. - -- Mon Jun 4 08:23:43 MEST 2007 - - Small changes for EM logging - -- Pest Jun 10, 23:16:23 MEST 2007 - - Set wrong values in 62_EMEM to -1 - -- Pest Jun 12, 21:33:00 MEST 2007 - - in 62_EMEM.pm: added energy_today and energy_total - -- Pest Jun 18, 20:06:23 MEST 2007 - - in 62_EMEM.pm: Power units removed from value content added to name. - -- Rudi Sun Aug 5 10:59:51 MEST 2007 - - WS300 Loglevel changed for KS300 device (from 2 to GetLogLevel or 5) - - First version of the FritzBox port: - - Perl binary/ftdi_sio module - - EM: added setTime, reset - - docs/fritzbox.html. Note: The fb_fhem_0.1.tar.gz won't be part of CVS - as it contains largee binaries (swapfile, perl mipsel executable, etc). - -- Rudi Mon Aug 6 20:15:00 MEST 2007 - - archiving added to the main logs. - NOTE: The FileLog filename (INT attribute) is now also called logfile. - -- Rudi Wed Aug 29 08:28:34 MEST 2007 - - archive attributes clarified in the doc - -- Rudi Mon Sep 3 15:47:59 MEST 2007 - - 99_Sunrise_EL.pm checked in. Replaces 99_Sunrise.pm, and does not need - any Date module. - -- Rudi Sun Sep 9 08:43:03 MEST 2007 - - mode holiday_short added + documentation. Not tested. - any Date module. - -- Rudi Wed Oct 3 18:21:36 MEST 2007 - - weblinks added. Used by webpgm2 to display more than one plot at once - - webpgm2 output reformatted. Using CSS to divide the screen area in 3 - parts: command line, room-list and rest - -- Dirk Wed Oct 7 12:45:09 MEST 2007 - - FHT lime-protection code discovered - -- Dirk Wed Oct 18 23:28:00 MEST 2007 - - Softwarebuffer for FHT devices with queuing unsent commands and - repeating commands by transmission failure - - FHT low temperatur warning and setting for lowtemp-offset - - Change naming for state into warnings - Tagged as dirkh_20071019_0 - -- Martin Fri Dec 21 13:39:17 CET 2007 - - voip2fhem added (contrib/) - -- Peter Sun Dec 23 19:59:00 MEST 2007 - - linux.html: Introduction refinement. - -- Rudi Sat Dec 29 16:27:14 MET 2007 - - delattr renamed to deleteattr, as del should complete to delete and not to - delattr - - defattr renamed to setdefaultattr (same as before for def) - - devicespec introduced: - it may contain a list of devices, a range of devices, or multiple devices - identified by regexp. Following commands take a devicespec as argument: - attr, deleteattr, delete, get, list, set, setstate, trigger - -- Boris Sat Dec 29 16:56:00 CET 2007 - - %NAME, %EVENT, %TYPE parameters in notify definition, commandref.html update - -- Boris Sun Dec 30 22:35:00 CET 2007 - - added dblog/93_DbLog.pm and samples in contrib directory, commandref.html - update - -- Rudi Mon Dec 31 15:37:19 MET 2007 - - feature: webfrontend/pgm2 converted to a FHEM module - No more need for a webserver for basic WEB administration. For HTTPS or - password you still need apache or the like. - One step closer for complete fhem on the FritzBox. - -- Boris Sun Jan 06 13:35:00 CET 2008 - - bugfix: 62_EMEM.pm: changed reading energy_total_kWh to energy_kWh_w, - added energy_kWh (formerly energy_total_kWh) - - changed em1010.pl accordingly, added em1000em doc for getDevStatus reply - from device - - minor changes in fhem.html - -- Rudi Tue Jan 8 21:13:08 MET 2008 - - feature: attr global allowfrom <ip-adresses/hostnames> - If set, only connects from these addresses are allowed. This is to - "simulate" a little bit of security. - -- Rudi Sat Jan 19 18:04:12 MET 2008 - - FHT: multiple commands - Up to 8 commands in one set, these are transmitted at once to the FHT - - softbuffer changes - minfhtbuffer attribute, as otherwise nearly everything will be sent to - the FHT buffer, so ordering won't take effect. - - cmd rename - report1,report2. refreshvalues changed to report1 and report2. refreshvalues - won't be advertized but still replaced with "report1 255 report2 255" - - extensive documentation update for the FHT - - lime-protection changed, as it is an actuator subcommand. Further actuator - commands added. - -- Rudi Sun Jan 27 18:12:42 MET 2008 - - em1010PC: sending a "67" after a reset skips the manual interaction: - automatic reset is now possible. - -- Peter S. Sat Feb 16 22:22:21 MET 2008 - - linux.html: Note on kernel 2.6.24.2 (includes our changes) - -- Peter S. Wed Mar 19 08:24:00 MET 2008 - - 00_FHZ.pm: DoTriger -> DoTrigger - -- Rudi Fri May 9 20:00:00 MEST 2008 - - feature: FHEM modules may live on a filesystem with "ignorant" casing (FAT) - If you install FHEM on a USB-Stick (e.g. for the FritzBox) it may happen - that the filename casing is different from the function names inside the - file. - -> Fhem won't find the <module>_Initialize function. Fixed by searching all - function-names for a match with "ignore-case" - - feature: FileLog function "set reopen" impemented. In case you want to - delete some wrong entries from a current logfile, you must tell fhem to - reopen the file again - - feature: multiline commands are supported through the command line - Up till now multiline commands were supported only by "include". Now they - are supprted from the (tcp/ip) connection too, so they can be used by the - web frontends. - - feature: pgm2 installation changes, multiple instances, external css - pgm2 (FHEMWEB) is now a "real" fhem module: - - the configuration takes place via attributes - - the css file is external, and each FHEMWEB instance can use its own set - - the default location for pictures, gnuplot scripts and css is the FHEM - module directory - - multiline support for notify and at scripts. - - feature: FileLog "set reopen" for manual tweaking of logfiles. - - feature: multiline commands are supported through the command line - - feature: pgm2 installation changes, multiple instances, external css - --tdressler Sa May 10 23:00:00 MEST 2008 - - feature:add WS2000 Support new modul 87_ws2000.pm and standalone - reader/server ws2000_reader.pl - - doc: modified fhem.html/commandref.html reflectiing ws2000 device and - added windows support (tagged:before tdressler_20080510_1, after - tdressler_20080510_2) - --tdressler So May 11 19:30:00 MEST 2008 - - feature: add ReadyFn to fhem.pl in main loop to have an alternative for - select, which is not working on windows (thomas 11.05) - - feature: set timeout to 0.2s, if HandleTimeout returns undef=forever - (tagged tdressler_20080511_1/2) - - bugfix : WS2000:fixed serial port access on windows by replacing FD with - ReadyFn - - bugfix : FileLog: dont use FH->sync on windows (not implemented there) - - feature: EM, WS300, FHZ:Add Switch for Device::SerialPort and - Win32::SerialPort to get it running in Windows (sorry, untestet) - - -tdressler So May 11 23:30:00 MEST 2008 - - bugfix: FileLog undefined $data in FileLog_Get - - feature: fhem.pl check modules for compiletime errors and do not initialize - them if any - - bugfix: EM, WS300, FHZ scope of portobj variable - - -tdressler Mo May 12 14:00:00 MEST 2008 - - bugfix: FHZ with windows, use there ReadyFn if windows; small cosmetic - changes - - doc: add hint to virtual com port driver, modification for FHZ to use - default FTDI driver - - -tdressler Mo May 12 19:00:00 MEST 2008 - - feature : add windows support to M232 - --tdressler So May 18 13:30:00 MEST 2008 - - feature : add ELV IPWE1 support - -- Peter S. Mon Jun 02 00:39 MET 2008 - - linux.html: openSUSE 11 contains our changes. - -- Thu Jun 12 07:15:03 MEST 2008 - - feature: FileLog get to read logfiles / webpgm2: gnuplot-scroll mode to - navigate/zoom in logfiles - webpgm2 uses the FileLog get to grep data for a given data range from a - logfile. Using this grep scrolling to a different date range / zooming - to another resolution (day/week/month/year) can be implemented. - The logfiles should be large, as scrolling across logfiles is not - implemented. To speed up the grep, a binary search with seek is used, and - seek positions are cached. - -- Thu Jul 11 07:15:03 MEST 2008 - - feature: 99_SVG.pm for webpgm2: generates SVG from the logfile. - Generating SVG is configurable, the "old" gnuplot mode is still there. - Downside of the SVG: the browser must support SVG (Firefox/Opera does, - I.E. with the Adobe plugin), and the browsesr consumes more memory. - Upside: no gnuplot needed on the server, faster(?), less data to transfer - for daily data. - Tested with Firefox 3.0. - Todo: Test with IE+Adobe Plugin/Opera. - - feature: HOWTO for webpgm2 (first chapter) - -- Fri Jul 25 18:14:26 MEST 2008 - - Autoloading modules. In order to make module installation easier and - to optimize memory usage, modules are loaded when the first device of a - certain category is defined. Exceptions are the modules prefixed with 99, - these are considered "utility" modules and are loaded at the beginning. - Some of the older 99_x modules were renamed (99_SVG, 99_dummy), and most - contrib modules were moved to the main FHEM directory. - -- Boris Sat Nov 01 CET 2008 - - feature: new commands fullinit and reopen for FHZ, commandref.html update - - bugfix: avoid access to undefined NotifyFn in hash in fhem.pl - -- Boris Sun Nov 02 CET 2008 - - feature: new modules 00_CM11.pm and 20_X10.pm for integration of X10 - devices in fhem - - feature: X10 support for pgm3 - -- Sat Nov 15 10:23:56 MET 2008 (Rudi) - - Watchdog crash fixed: watchdog could insert itself more than once in the - internal timer queue. The first one deletes all occurances from the list, - but the loop over the list works on the cached keys -> the function/arg for - the second key is already removed. - - feature: X10 support for pgm3 - -- Boris Sat Nov 15 CET 2008 - - bugfix: correct correction factors for EMEM in 15_CUL_EM.pm - -- Wed Dec 3 18:36:56 MET 2008 (Rudi) - - reorder commandref.html, so that all aspects of a device - (define/set/get/attributes) are in one block. This makes possible to - "outsource" device documentation - - added "mobile" flag to the CUL definition, intended for a CUR, which is - a remote with a battery, so it is not connected all the time to fhem. - Without the flag fhem will block when the CUR is disconnected. - Note: we have to sleep after disconnect for 5 seconds, else the Linux - kernel sends us a SIGSEGV, and the USB device is gone till the next reboot. - - the fhem CUL part documented - -- Sun Dec 7 21:09 (Boris) - - reworked 15_CUL_EM.pm to account for timer wraparounds, more readings added - - speed gain through disabled refreshvalues query to all FHTs at definition; - if you want it back at a "set myFHT report1 255 report2 255" command to the - config file. - -- Mon Dec 8 21:26 MET 2008 (Rudi) - - Modules can now modify the cmds hash, i.e. modules can add / overwrite / - delete internal fhem commands. See 99_XmlList.pm for an example. Since this - modules is called 99_xxx, it will be always loaded, but user of webpgm2 do - not need it. - -- Wed Dec 17 19:48 (Boris) - - attribute rainadjustment for KS300 in 13_KS300.pm to account for random - switches in the rain counter (see commandref.html) - -- Fri Jan 2 10:29 2009 (Rudi) - - 00_CUL responds to CUR request. These are sent as long FS20 messages, with - a housecode stored in CUR_id_list attribute of the CUL device. If the ID - matches, the message is analyzed, and an FS20 message to the same address - is sent back. The CUR must have reception enabled. - Right now status/set time/set FHT desired temp are implemented. - -- Fri Jan 6 (Boris) - - daily/monthly cumulated values for EMWZ/EMGZ/EMWM with 15_CUL_EM by Klaus - -- Fri Jan 9 - - Added a unified dispatch for CUL/FHZ and CM11, since all of them used the - same code. - - - Addedd IODev attribute to FS20/FHT/HMS/KS300/CUL_WS/CUL/EMWZ/EMGZ/EMEM -- Sun Jan 11 (Klaus) - - Added fixedrange option day|week|month|year (for pgm2) - e.g.: attr wlEnergiemonat fixedrange month - - Added multiple room assignments for one device (for pgm2): - e.g.: attr Heizvorlauftemp room Energie,Heizung - - Added attr title and label(s) for more flexible .gplot files (for pgm2) - e.g.: attr wl_KF title "Fenster:".$value{KellerFenster}.", Entfeuchter: ".$value{Entfeuchter} - .gplot: <TL> (is almost there!) - attr wl_KF label "Fenster":"Entfeuchter" - .gplot: <L0> ... <L9> (constant text is to be replaced individually) - - Added attr global logdir, used by wildcard %ld in perl.pm - e.g.: attr global logdir /var/tmp - define emGaslog FileLog %ld/emGas.log emGas:.*CNT.* - -- Sat Feb 15 2009 (Boris) - - added counter differential per time in 81_M232Counter.pm, commandref.html - updated - -- Thu Mar 29 2009 (MartinH) - - pgm3: bugfix, format table for userdef - - pgm3: feature X10_support, taillogorder optional with date - - pgm3: HMS100CO added, fhem.html relating pgm3 updated - -- Sat May 30 2009 (Rudi) - - 99_SUNRISE_EL: sunrise/sunset called in "at" computes correctly the next - event. New "sunrise()/sunset()" calls added, min max optional parameter. - -- Sun May 31 2009 (Boris) - - 81_M232Counter.pm: counter stops at 65536; workaround makes counter wraparound - -- Mon Jun 01 2009 (Boris) - - 59_Weather.pm: new virtual device for weather forecasts, documentation - updated. - -- Tue Jun 09 2009 (Boris) - - 11_FHT.pm: lazy attribute for FHT devices - -- Sun Jun 14 2009 (Rudi) - - 11_FHT.pm: tmpcorr attribute for FHT devices - -- Sat Jun 20 2009 (Boris) - - 09_USF1000.pm: new module to support USF1000S devices. - -- Fri Aug 08 2009 (Boris) - - 09_USF1000.pm: suppress inplausible readings from USF1000 - -- Sat Sep 12 2009 (Boris) - - 00_CM11.pm: feature: get time, fwrev, set reopen for CM11 (Boris 2009-09-12) - -- Sun Sep 20 2009 (Boris) - - Module 09_BS.pm for brightness sensor added (Boris 2009-09-20) - -- Sat Oct 03 2009 (Boris) - - bugfix: missing blank in attribute list for FHT; exclude report from lazy - - typos and anchors in documentation corrected - -- Sun Oct 11 2009 (Boris) - - finalized 09_BS.pm and documentation - -- Tue Nov 10 2009 (Martin Haas) - - Bugfix: pgm3: Pulldown-Menu without selected FHTDEV not possible any more - -- Thu Nov 12 2009 (Rudi) - - The duplicate pool added. The check routine is called from the Dispatch - routine (so it will affecc CUL/FHZ and CM11), and for FS20 calls from - the CUL and FHZ Write function. - Duplicates within 0.5 seconds are filtered if they are not reported by the - same IO Unit. Existing check for IODev removed from BS USF1000 FS20 FHT HMS - KS300 CUL_WS CUL_EM X10. - -- Mon Nov 16 2009 (MartinH) - - pgm3: Google-Weather-Api added. Display of all Logs including the - FS20-devices (grep on fhem.log) The status of the batteries of FHT and HMS - are shown in the graphics. php4 disabled. Now only php5 is supported. A - lot of examples of the UserDefs are added. The pgm3-section of fhem.html was - changed. - -- Sat Dec 19 2009 (MartinH) - - pgm3: Automatic support for CUL_WS (S300TH) added - -- Mon Dec 21 2009 (Rudi) - - In order to support automatic device creation (coming 98_autocreate.pm), - the return value in case of an undefined device should contain parameters - for a correct define statement. - -- Fri Jan 1 2010 - - my %defptr is no $modules{modname}{defptr} in order for CommandReload to - work. There is also a second parameter $modules{modname}{ldata} which will - be saved over a Reload, used by the FS20 for the follow feature. - - ignore attribute added to ignore devices of the neighbour - -- Fri Jan 8 2010 (MartinH) - Interface to fhem changes to stream_socket_client. Table-format on - Android-Browser optimized. Optimized for smartphones. Rooms possible for - Webcam and Google-Weather. Output of html better formated and skinable -- - change the colors. - -- Sat Feb 6 2010 (Boris) - - feature: on-for-timer added for X10 modules and bug fixed for overlapping - on-till and on-for-timer commands (Boris) - -- Thu Jun 30 2011 (Maz Rashid) - - Introducing 00_TUL.pm and 10_EIB.pm modules for connecting FHEM on EIB. - -- Thu Aug 04 2011 (Boris) - - enabled logging for 59_Weather.pm - -- Thu Dec 01 2011 (Martin F.) - - Move JsonList from contrib to main modules. Jsonlist output is optimized - and now be more structured. - -- Sun Jan 29 2012 (Maz Rashid) - - Improving 10_EIB.pm by introducing Get and interpreting received value according - to the selected model (based on datapoint types.) - - Introduced documentation for TUL / EIB modules. - -- Fr Feb 24 2012 (Willi) - - New modules TRX for RFXCOM RFXtrx transceiver - -- So May 20 2012 (M. Fischer) - - Added support for a cleaner installation of pgm2 via updatefhem. - -- Sa May 26 2012 (M. Fischer) - - Added new command backup to separate this feature from updatefhem. - -- Thu May 31 2012 (M. Fischer) - - Added new global attribute <backupsymlink> - -- Fri Jun 01 2012 (M. Fischer) - - Added new global attribute <backupcmd> - -- Sat Jun 02 2012 (M. Fischer) - - Added new global attribute <backup_before_update> - - Backuproutine from updatefhem removed. updatefhem use the command backup from now. - -- Sun Jun 03 2012 (M. Fischer) - - Added new global attribute <exclude_from_update> - - Added new parameter <changed> to updatefhem - -- Sun Jun 17 2012 (M. Fischer) - - CULflash routine from updatefhem removed. CULflash is a standalone module from now. - -- Sat Aug 11 2012 (M. Fischer) - - Added new module IPCAM - -- Tue Feb 19 2013 (Johannes) - - added new Javascript Frontend based on ExtJS (by Johannes) - - added example Setup SQL and configuration for SQLite - - extended the MySQL Setup SQL to use 512 characters in EVENT column - -- Sun Jun 23 2013 (UliM) - - Added new module "remotecontrol" - -- Sun Jul 14 2013 (Alexus) - - Added new module "EGPM2LAN" and "EGPM" - -- Fri Jul 26 2013 (Dirk) - - Added new module "I2C_BMP180" - -- Fri Jul 26 2013 (A. Vogt alias baumrasen) - - Added new module "WWO" - -- Mon Jan 13 2014 (A. Schulz alias hexenmeister) - - Added new module "SYSMON" - -- Fri Jan 16 2014 (andreas-fey) - - Added new module "pilight" - -- Sat Jan 18 2014 (tobiasfaust) - - Added new Module "Text2Speech" - -- Sat Feb 16 2014 (immiimmi) - - Added new Module "THZ" - -- Wed Feb 25 2014 (andreas-fey) - - Update on pilight module for more protocols - -- Fri Mar 07 2014 (betateilchen) - - First officiel release of configDB via update process - -- Mon Mar 24 2014 (betateilchen) - - old module 98_PID.pm moved to contrib - will be replaced by 98_PID20.pm in next major release - -- Wed Mar 26 2014 (John / betateilchen) - - added new module 98_PID20.pm as announced replacement for old 98_PID.pm - -- Sun Mar 30 2014 (C-HERRMANN) - - added new module 10_UNIRoll.pm - -- Sun Apr 24 2014 (kaihs) - - added new module 02_FRAMEBUFFER.pm - - added new module 51_TSL2561.pm - -- Thu May 08 2014 (tobiasfaust) - - added new global modules function $hash->{DbLog_splitFn} - to let split the generated events by the own module - into readingsname, value and unit - - added new module contrib/97_SprinkleControl.pm - - added new module contrib/98_Sprinkle.pm - -- Thu Jun 12 2014 (rr2000) - - added new module 37_SHC.pm - - added new module 37_SHCdev.pm to support smarthomatic devices - -- Wed Jun 18 2014 (heikoranft) - - added new module 89_HEATRONIC.pm - -- Fri Jun 20 2014 (hofrichter) - - added new module 70_PIONEERAVR.pm to support Pioneer AV receivers - - added new module 71_PIONEERAVRZONE.pm to support zones of PIONEER AV receivers - -- Mon Jun 30 2014 (thomyd) - - added new module 10_SOMFY.pm to support Somfy RTS blinds - -- Sun Aug 03 2014 (xusader) - - added new module 70_PushNotifier.pm to support Push Messages with PushNotifier - -- Mon Sep 22 2014 (creideiki) - - added new module 34_NUT to support the Network UPS Tools - -- Tue Sep 23 2014 (eisler) - - added new module 44_TEK603 to support the TEK603 Eco Monitor - +- Rudi, Thu Feb 1 13:27:15 MET 2007 + Created the file HISTORY and the file README.DEV + +- Pest, Thu Feb 1 20:45 MET 2007 + Added description for attribute , + +- Rudi, Sun Feb 11 18:56:05 MET 2007 + - showtime added for pgm2 (useful for FS20 piri display) + - defattr command added, it makes easier to assign room, type, etc for a group + of devices. + - em1010.pl added to the contrib directory. It seems that we are able + to read out the EM1010PC. Not verified with multiple devices. + +- Pest, Thu Feb 11 23:35 MET 2007 + - Added doc/linux.html (multiple USDB devices, udev links) + - Linked fhem.html and commandref.html to linux.html + +- Martin Haas, Fri Feb 23 10:18 MET 2007 + - ARM-Section (NSLU2) added to doc/linux.html + +- Pest, Sat Feb 24 18:30 MET 2007 + - doc/linux.html: Module build re-written. + +- Rudi, Sun Mar 4 11:18:10 MET 2007 + Reorganization. Goal: making attribute adding/deleting more uniform + (, + possible (i.e. saving the configfile, list of possible devices etc). + + Internal changes: + - %logmods,%devmods moved to %modules. Makes things more uniform + - %logs merged into %defs + - local state info (%readings) changed to global ($defs{$d}{READINGS}) + -> No need for the listfn function in each module + -> User written scripts can more easily analyze device states + + User visible changes: + - at/notify , + modules. Now it is possible + - to have a further , + (notify & filelog use the same interface) + - to have more than one notify for the same event + - to delete at commands without strange escapes. + The delete syntax changed (no more def/at/ntfy needed) + - at/notify can have attributes + Drawback: each at and notify must have a name, which is strange first. + - logfile/modpath/pidfile/port/verbose , + Dumping and extending these attributes is easier, no special handling + required in the web-frontend. + - savefile renamed to , + - configfile global attribute added. + - save command added, it writes the statefile and then the configfile. + - delattr added to delete single attributes + - list/xmllist format changed, they contain more information. + - , + in the same format. This data is contained in the xmllist. + - disable attribute for at/notify/filelog + - rename added + +- Rudi, Tue Mar 27 20:43:15 MEST 2007 + fhemweb.pl (webpgm2) changes: + - adopted to the new syntax + - better commandline support (return <pre> formatted) + - FileLog attribute logtype added, and 4 logtypes (== gnuplot files) + defined: fs20, fht, ks300_1, ks300_2 + - links in the commandref.html file added + - device dependent attribute and set support + +- Pest, Sun Apr 08 17:55:15 MEST 2007 + em1010.pl: + - Make difference between sensors 1..4 and 5.. + - Checked values for sensor 5 (cur.energy + cur.power) - ok + - Checked values for sensor 5 cur.energy is ok, cur.power still off. + Correction factor needs to be determined. + - setTime: Without argument, now the current time of the PC is used. + - setRperKW: Factor of 10 required, 75 U/kWh -> 750. + +- Pest, Tue Apr 10 20:31:22 MEST 2007 + em1010.pl: + - Introduced new double-word function (dw) + - getDevStatus: energy values kWh/h, kWh/d, total. + +- Rudi Sat Apr 14 10:34:36 MEST 2007 + final documentations, release 4.0. Tagged as FHEM_4_0 + +- Pest, Sat Apr 14 14:21:00 MEST 2007 + - doc: linux.html (private udev-rules, not 50-..., ATTRS) + +- Pest, Sun Apr 15 14:54:30 MEST 2007 + - doc: fhem.pl and commandref.html (notifyon -> notify, correction of examples) + +- Rudi, Tue Apr 24 08:10:43 MEST 2007 + - feature: modify command added. It helps change e.g. only the time component + for an at command, without deleting and creating it again and then + reapplying all the attributes. + - feature: the , + instead. The - is used to separate ranges in the set command. + +- Rudi, Sun May 27 12:51:52 MEST 2007 + - Archiving FileLogs. Added fhemweb.pl (pgm2) code, to show logs from the + archive directory. See the attributes archivedir/archivecmd. + - Added EM1010PC suppoort (right now only with EM1000WZ). Support added + for displaying logs in the fhemweb.pl (webfrontends/pgm2) + +- Pest, Mon May 28 19:39:22 MEST 2007 + - Added 62_EMEM.pm to support EM1000-EM devices. + - doc: Update of commandref.htm (typos and EMEM). + +- Pest, Mon May 29 00:07:00 MEST 2007 + - check-in changes of 60_EM.pm to make EMEM work. + +- Mon Jun 4 08:23:43 MEST 2007 + - Small changes for EM logging + +- Pest Jun 10, 23:16:23 MEST 2007 + - Set wrong values in 62_EMEM to -1 + +- Pest Jun 12, 21:33:00 MEST 2007 + - in 62_EMEM.pm: added energy_today and energy_total + +- Pest Jun 18, 20:06:23 MEST 2007 + - in 62_EMEM.pm: Power units removed from value content added to name. + +- Rudi Sun Aug 5 10:59:51 MEST 2007 + - WS300 Loglevel changed for KS300 device (from 2 to GetLogLevel or 5) + - First version of the FritzBox port: + - Perl binary/ftdi_sio module + - EM: added setTime, reset + - docs/fritzbox.html. Note: The fb_fhem_0.1.tar.gz won't be part of CVS + as it contains largee binaries (swapfile, perl mipsel executable, etc). + +- Rudi Mon Aug 6 20:15:00 MEST 2007 + - archiving added to the main logs. + NOTE: The FileLog filename (INT attribute) is now also called logfile. + +- Rudi Wed Aug 29 08:28:34 MEST 2007 + - archive attributes clarified in the doc + +- Rudi Mon Sep 3 15:47:59 MEST 2007 + - 99_Sunrise_EL.pm checked in. Replaces 99_Sunrise.pm, and does not need + any Date module. + +- Rudi Sun Sep 9 08:43:03 MEST 2007 + - mode holiday_short added + documentation. Not tested. + any Date module. + +- Rudi Wed Oct 3 18:21:36 MEST 2007 + - weblinks added. Used by webpgm2 to display more than one plot at once + - webpgm2 output reformatted. Using CSS to divide the screen area in 3 + parts: command line, room-list and rest + +- Dirk Wed Oct 7 12:45:09 MEST 2007 + - FHT lime-protection code discovered + +- Dirk Wed Oct 18 23:28:00 MEST 2007 + - Softwarebuffer for FHT devices with queuing unsent commands and + repeating commands by transmission failure + - FHT low temperatur warning and setting for lowtemp-offset + - Change naming for state into warnings + Tagged as dirkh_20071019_0 + +- Martin Fri Dec 21 13:39:17 CET 2007 + - voip2fhem added (contrib/) + +- Peter Sun Dec 23 19:59:00 MEST 2007 + - linux.html: Introduction refinement. + +- Rudi Sat Dec 29 16:27:14 MET 2007 + - delattr renamed to deleteattr, as del should complete to delete and not to + delattr + - defattr renamed to setdefaultattr (same as before for def) + - devicespec introduced: + it may contain a list of devices, a range of devices, or multiple devices + identified by regexp. Following commands take a devicespec as argument: + attr, deleteattr, delete, get, list, set, setstate, trigger + +- Boris Sat Dec 29 16:56:00 CET 2007 + - %NAME, %EVENT, %TYPE parameters in notify definition, commandref.html update + +- Boris Sun Dec 30 22:35:00 CET 2007 + - added dblog/93_DbLog.pm and samples in contrib directory, commandref.html + update + +- Rudi Mon Dec 31 15:37:19 MET 2007 + - feature: webfrontend/pgm2 converted to a FHEM module + No more need for a webserver for basic WEB administration. For HTTPS or + password you still need apache or the like. + One step closer for complete fhem on the FritzBox. + +- Boris Sun Jan 06 13:35:00 CET 2008 + - bugfix: 62_EMEM.pm: changed reading energy_total_kWh to energy_kWh_w, + added energy_kWh (formerly energy_total_kWh) + - changed em1010.pl accordingly, added em1000em doc for getDevStatus reply + from device + - minor changes in fhem.html + +- Rudi Tue Jan 8 21:13:08 MET 2008 + - feature: attr global allowfrom <ip-adresses/hostnames> + If set, only connects from these addresses are allowed. This is to + "simulate" a little bit of security. + +- Rudi Sat Jan 19 18:04:12 MET 2008 + - FHT: multiple commands + Up to 8 commands in one set, these are transmitted at once to the FHT + - softbuffer changes + minfhtbuffer attribute, as otherwise nearly everything will be sent to + the FHT buffer, so ordering won't take effect. + - cmd rename + report1,report2. refreshvalues changed to report1 and report2. refreshvalues + won't be advertized but still replaced with "report1 255 report2 255" + - extensive documentation update for the FHT + - lime-protection changed, as it is an actuator subcommand. Further actuator + commands added. + +- Rudi Sun Jan 27 18:12:42 MET 2008 + - em1010PC: sending a "67" after a reset skips the manual interaction: + automatic reset is now possible. + +- Peter S. Sat Feb 16 22:22:21 MET 2008 + - linux.html: Note on kernel 2.6.24.2 (includes our changes) + +- Peter S. Wed Mar 19 08:24:00 MET 2008 + - 00_FHZ.pm: DoTriger -> DoTrigger + +- Rudi Fri May 9 20:00:00 MEST 2008 + - feature: FHEM modules may live on a filesystem with "ignorant" casing (FAT) + If you install FHEM on a USB-Stick (e.g. for the FritzBox) it may happen + that the filename casing is different from the function names inside the + file. + -> Fhem won't find the <module>_Initialize function. Fixed by searching all + function-names for a match with "ignore-case" + - feature: FileLog function "set reopen" impemented. In case you want to + delete some wrong entries from a current logfile, you must tell fhem to + reopen the file again + - feature: multiline commands are supported through the command line + Up till now multiline commands were supported only by "include". Now they + are supprted from the (tcp/ip) connection too, so they can be used by the + web frontends. + - feature: pgm2 installation changes, multiple instances, external css + pgm2 (FHEMWEB) is now a "real" fhem module: + - the configuration takes place via attributes + - the css file is external, and each FHEMWEB instance can use its own set + - the default location for pictures, gnuplot scripts and css is the FHEM + module directory + - multiline support for notify and at scripts. + - feature: FileLog "set reopen" for manual tweaking of logfiles. + - feature: multiline commands are supported through the command line + - feature: pgm2 installation changes, multiple instances, external css + +-tdressler Sa May 10 23:00:00 MEST 2008 + - feature:add WS2000 Support new modul 87_ws2000.pm and standalone + reader/server ws2000_reader.pl + - doc: modified fhem.html/commandref.html reflectiing ws2000 device and + added windows support (tagged:before tdressler_20080510_1, after + tdressler_20080510_2) + +-tdressler So May 11 19:30:00 MEST 2008 + - feature: add ReadyFn to fhem.pl in main loop to have an alternative for + select, which is not working on windows (thomas 11.05) + - feature: set timeout to 0.2s, if HandleTimeout returns undef=forever + (tagged tdressler_20080511_1/2) + - bugfix : WS2000:fixed serial port access on windows by replacing FD with + ReadyFn + - bugfix : FileLog: dont use FH->sync on windows (not implemented there) + - feature: EM, WS300, FHZ:Add Switch for Device::SerialPort and + Win32::SerialPort to get it running in Windows (sorry, untestet) + + -tdressler So May 11 23:30:00 MEST 2008 + - bugfix: FileLog undefined $data in FileLog_Get + - feature: fhem.pl check modules for compiletime errors and do not initialize + them if any + - bugfix: EM, WS300, FHZ scope of portobj variable + + -tdressler Mo May 12 14:00:00 MEST 2008 + - bugfix: FHZ with windows, use there ReadyFn if windows; small cosmetic + changes + - doc: add hint to virtual com port driver, modification for FHZ to use + default FTDI driver + + -tdressler Mo May 12 19:00:00 MEST 2008 + - feature : add windows support to M232 + +-tdressler So May 18 13:30:00 MEST 2008 + - feature : add ELV IPWE1 support + +- Peter S. Mon Jun 02 00:39 MET 2008 + - linux.html: openSUSE 11 contains our changes. + +- Thu Jun 12 07:15:03 MEST 2008 + - feature: FileLog get to read logfiles / webpgm2: gnuplot-scroll mode to + navigate/zoom in logfiles + webpgm2 uses the FileLog get to grep data for a given data range from a + logfile. Using this grep scrolling to a different date range / zooming + to another resolution (day/week/month/year) can be implemented. + The logfiles should be large, as scrolling across logfiles is not + implemented. To speed up the grep, a binary search with seek is used, and + seek positions are cached. + +- Thu Jul 11 07:15:03 MEST 2008 + - feature: 99_SVG.pm for webpgm2: generates SVG from the logfile. + Generating SVG is configurable, the "old" gnuplot mode is still there. + Downside of the SVG: the browser must support SVG (Firefox/Opera does, + I.E. with the Adobe plugin), and the browsesr consumes more memory. + Upside: no gnuplot needed on the server, faster(?), less data to transfer + for daily data. + Tested with Firefox 3.0. + Todo: Test with IE+Adobe Plugin/Opera. + - feature: HOWTO for webpgm2 (first chapter) + +- Fri Jul 25 18:14:26 MEST 2008 + - Autoloading modules. In order to make module installation easier and + to optimize memory usage, modules are loaded when the first device of a + certain category is defined. Exceptions are the modules prefixed with 99, + these are considered "utility" modules and are loaded at the beginning. + Some of the older 99_x modules were renamed (99_SVG, 99_dummy), and most + contrib modules were moved to the main FHEM directory. + +- Boris Sat Nov 01 CET 2008 + - feature: new commands fullinit and reopen for FHZ, commandref.html update + - bugfix: avoid access to undefined NotifyFn in hash in fhem.pl + +- Boris Sun Nov 02 CET 2008 + - feature: new modules 00_CM11.pm and 20_X10.pm for integration of X10 + devices in fhem + - feature: X10 support for pgm3 + +- Sat Nov 15 10:23:56 MET 2008 (Rudi) + - Watchdog crash fixed: watchdog could insert itself more than once in the + internal timer queue. The first one deletes all occurances from the list, + but the loop over the list works on the cached keys -> the function/arg for + the second key is already removed. + - feature: X10 support for pgm3 + +- Boris Sat Nov 15 CET 2008 + - bugfix: correct correction factors for EMEM in 15_CUL_EM.pm + +- Wed Dec 3 18:36:56 MET 2008 (Rudi) + - reorder commandref.html, so that all aspects of a device + (define/set/get/attributes) are in one block. This makes possible to + "outsource" device documentation + - added "mobile" flag to the CUL definition, intended for a CUR, which is + a remote with a battery, so it is not connected all the time to fhem. + Without the flag fhem will block when the CUR is disconnected. + Note: we have to sleep after disconnect for 5 seconds, else the Linux + kernel sends us a SIGSEGV, and the USB device is gone till the next reboot. + - the fhem CUL part documented + +- Sun Dec 7 21:09 (Boris) + - reworked 15_CUL_EM.pm to account for timer wraparounds, more readings added + - speed gain through disabled refreshvalues query to all FHTs at definition; + if you want it back at a "set myFHT report1 255 report2 255" command to the + config file. + +- Mon Dec 8 21:26 MET 2008 (Rudi) + - Modules can now modify the cmds hash, i.e. modules can add / overwrite / + delete internal fhem commands. See 99_XmlList.pm for an example. Since this + modules is called 99_xxx, it will be always loaded, but user of webpgm2 do + not need it. + +- Wed Dec 17 19:48 (Boris) + - attribute rainadjustment for KS300 in 13_KS300.pm to account for random + switches in the rain counter (see commandref.html) + +- Fri Jan 2 10:29 2009 (Rudi) + - 00_CUL responds to CUR request. These are sent as long FS20 messages, with + a housecode stored in CUR_id_list attribute of the CUL device. If the ID + matches, the message is analyzed, and an FS20 message to the same address + is sent back. The CUR must have reception enabled. + Right now status/set time/set FHT desired temp are implemented. + +- Fri Jan 6 (Boris) + - daily/monthly cumulated values for EMWZ/EMGZ/EMWM with 15_CUL_EM by Klaus + +- Fri Jan 9 + - Added a unified dispatch for CUL/FHZ and CM11, since all of them used the + same code. + + - Addedd IODev attribute to FS20/FHT/HMS/KS300/CUL_WS/CUL/EMWZ/EMGZ/EMEM +- Sun Jan 11 (Klaus) + - Added fixedrange option day|week|month|year (for pgm2) + e.g.: attr wlEnergiemonat fixedrange month + - Added multiple room assignments for one device (for pgm2): + e.g.: attr Heizvorlauftemp room Energie,Heizung + - Added attr title and label(s) for more flexible .gplot files (for pgm2) + e.g.: attr wl_KF title "Fenster:".$value{KellerFenster}.", Entfeuchter: ".$value{Entfeuchter} + .gplot: <TL> (is almost there!) + attr wl_KF label "Fenster":"Entfeuchter" + .gplot: <L0> ... <L9> (constant text is to be replaced individually) + - Added attr global logdir, used by wildcard %ld in perl.pm + e.g.: attr global logdir /var/tmp + define emGaslog FileLog %ld/emGas.log emGas:.*CNT.* + +- Sat Feb 15 2009 (Boris) + - added counter differential per time in 81_M232Counter.pm, commandref.html + updated + +- Thu Mar 29 2009 (MartinH) + - pgm3: bugfix, format table for userdef + - pgm3: feature X10_support, taillogorder optional with date + - pgm3: HMS100CO added, fhem.html relating pgm3 updated + +- Sat May 30 2009 (Rudi) + - 99_SUNRISE_EL: sunrise/sunset called in "at" computes correctly the next + event. New "sunrise()/sunset()" calls added, min max optional parameter. + +- Sun May 31 2009 (Boris) + - 81_M232Counter.pm: counter stops at 65536; workaround makes counter wraparound + +- Mon Jun 01 2009 (Boris) + - 59_Weather.pm: new virtual device for weather forecasts, documentation + updated. + +- Tue Jun 09 2009 (Boris) + - 11_FHT.pm: lazy attribute for FHT devices + +- Sun Jun 14 2009 (Rudi) + - 11_FHT.pm: tmpcorr attribute for FHT devices + +- Sat Jun 20 2009 (Boris) + - 09_USF1000.pm: new module to support USF1000S devices. + +- Fri Aug 08 2009 (Boris) + - 09_USF1000.pm: suppress inplausible readings from USF1000 + +- Sat Sep 12 2009 (Boris) + - 00_CM11.pm: feature: get time, fwrev, set reopen for CM11 (Boris 2009-09-12) + +- Sun Sep 20 2009 (Boris) + - Module 09_BS.pm for brightness sensor added (Boris 2009-09-20) + +- Sat Oct 03 2009 (Boris) + - bugfix: missing blank in attribute list for FHT; exclude report from lazy + - typos and anchors in documentation corrected + +- Sun Oct 11 2009 (Boris) + - finalized 09_BS.pm and documentation + +- Tue Nov 10 2009 (Martin Haas) + - Bugfix: pgm3: Pulldown-Menu without selected FHTDEV not possible any more + +- Thu Nov 12 2009 (Rudi) + - The duplicate pool added. The check routine is called from the Dispatch + routine (so it will affecc CUL/FHZ and CM11), and for FS20 calls from + the CUL and FHZ Write function. + Duplicates within 0.5 seconds are filtered if they are not reported by the + same IO Unit. Existing check for IODev removed from BS USF1000 FS20 FHT HMS + KS300 CUL_WS CUL_EM X10. + +- Mon Nov 16 2009 (MartinH) + - pgm3: Google-Weather-Api added. Display of all Logs including the + FS20-devices (grep on fhem.log) The status of the batteries of FHT and HMS + are shown in the graphics. php4 disabled. Now only php5 is supported. A + lot of examples of the UserDefs are added. The pgm3-section of fhem.html was + changed. + +- Sat Dec 19 2009 (MartinH) + - pgm3: Automatic support for CUL_WS (S300TH) added + +- Mon Dec 21 2009 (Rudi) + - In order to support automatic device creation (coming 98_autocreate.pm), + the return value in case of an undefined device should contain parameters + for a correct define statement. + +- Fri Jan 1 2010 + - my %defptr is no $modules{modname}{defptr} in order for CommandReload to + work. There is also a second parameter $modules{modname}{ldata} which will + be saved over a Reload, used by the FS20 for the follow feature. + - ignore attribute added to ignore devices of the neighbour + +- Fri Jan 8 2010 (MartinH) + Interface to fhem changes to stream_socket_client. Table-format on + Android-Browser optimized. Optimized for smartphones. Rooms possible for + Webcam and Google-Weather. Output of html better formated and skinable -- + change the colors. + +- Sat Feb 6 2010 (Boris) + - feature: on-for-timer added for X10 modules and bug fixed for overlapping + on-till and on-for-timer commands (Boris) + +- Thu Jun 30 2011 (Maz Rashid) + - Introducing 00_TUL.pm and 10_EIB.pm modules for connecting FHEM on EIB. + +- Thu Aug 04 2011 (Boris) + - enabled logging for 59_Weather.pm + +- Thu Dec 01 2011 (Martin F.) + - Move JsonList from contrib to main modules. Jsonlist output is optimized + and now be more structured. + +- Sun Jan 29 2012 (Maz Rashid) + - Improving 10_EIB.pm by introducing Get and interpreting received value according + to the selected model (based on datapoint types.) + - Introduced documentation for TUL / EIB modules. + +- Fr Feb 24 2012 (Willi) + - New modules TRX for RFXCOM RFXtrx transceiver + +- So May 20 2012 (M. Fischer) + - Added support for a cleaner installation of pgm2 via updatefhem. + +- Sa May 26 2012 (M. Fischer) + - Added new command backup to separate this feature from updatefhem. + +- Thu May 31 2012 (M. Fischer) + - Added new global attribute <backupsymlink> + +- Fri Jun 01 2012 (M. Fischer) + - Added new global attribute <backupcmd> + +- Sat Jun 02 2012 (M. Fischer) + - Added new global attribute <backup_before_update> + - Backuproutine from updatefhem removed. updatefhem use the command backup from now. + +- Sun Jun 03 2012 (M. Fischer) + - Added new global attribute <exclude_from_update> + - Added new parameter <changed> to updatefhem + +- Sun Jun 17 2012 (M. Fischer) + - CULflash routine from updatefhem removed. CULflash is a standalone module from now. + +- Sat Aug 11 2012 (M. Fischer) + - Added new module IPCAM + +- Tue Feb 19 2013 (Johannes) + - added new Javascript Frontend based on ExtJS (by Johannes) + - added example Setup SQL and configuration for SQLite + - extended the MySQL Setup SQL to use 512 characters in EVENT column + +- Sun Jun 23 2013 (UliM) + - Added new module "remotecontrol" + +- Sun Jul 14 2013 (Alexus) + - Added new module "EGPM2LAN" and "EGPM" + +- Fri Jul 26 2013 (Dirk) + - Added new module "I2C_BMP180" + +- Fri Jul 26 2013 (A. Vogt alias baumrasen) + - Added new module "WWO" + +- Mon Jan 13 2014 (A. Schulz alias hexenmeister) + - Added new module "SYSMON" + +- Fri Jan 16 2014 (andreas-fey) + - Added new module "pilight" + +- Sat Jan 18 2014 (tobiasfaust) + - Added new Module "Text2Speech" + +- Sat Feb 16 2014 (immiimmi) + - Added new Module "THZ" + +- Wed Feb 25 2014 (andreas-fey) + - Update on pilight module for more protocols + +- Fri Mar 07 2014 (betateilchen) + - First officiel release of configDB via update process + +- Mon Mar 24 2014 (betateilchen) + - old module 98_PID.pm moved to contrib + will be replaced by 98_PID20.pm in next major release + +- Wed Mar 26 2014 (John / betateilchen) + - added new module 98_PID20.pm as announced replacement for old 98_PID.pm + +- Sun Mar 30 2014 (C-HERRMANN) + - added new module 10_UNIRoll.pm + +- Sun Apr 24 2014 (kaihs) + - added new module 02_FRAMEBUFFER.pm + - added new module 51_TSL2561.pm + +- Thu May 08 2014 (tobiasfaust) + - added new global modules function $hash->{DbLog_splitFn} + to let split the generated events by the own module + into readingsname, value and unit + - added new module contrib/97_SprinkleControl.pm + - added new module contrib/98_Sprinkle.pm + +- Thu Jun 12 2014 (rr2000) + - added new module 37_SHC.pm + - added new module 37_SHCdev.pm to support smarthomatic devices + +- Wed Jun 18 2014 (heikoranft) + - added new module 89_HEATRONIC.pm + +- Fri Jun 20 2014 (hofrichter) + - added new module 70_PIONEERAVR.pm to support Pioneer AV receivers + - added new module 71_PIONEERAVRZONE.pm to support zones of PIONEER AV receivers + +- Mon Jun 30 2014 (thomyd) + - added new module 10_SOMFY.pm to support Somfy RTS blinds + +- Sun Aug 03 2014 (xusader) + - added new module 70_PushNotifier.pm to support Push Messages with PushNotifier + +- Mon Sep 22 2014 (creideiki) + - added new module 34_NUT to support the Network UPS Tools + +- Tue Sep 23 2014 (eisler) + - added new module 44_TEK603 to support the TEK603 Eco Monitor + +- Wed Dec 17 2014 (Reinerlein) + - added new module 00_SONOS and 21_SONOSPLAYER to support the Sonos Multiroom Audiosystem + diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 419ede525..b0aea523c 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -1,324 +1,329 @@ -Files with a maintainer. If you wish to change a file, please contact the -maintainer of the file to do the change. - -The third column specifies, where/how the maintainer should be contacted. If -there is no reaction from the mainainer within 3 weeks, then rudolfkoenig -(forum.fhem.de/FHEM Forum) should be contacted, in order to assign a new -maintainer. - -When adding a new file, add yourself as the maintainer. - -File Maintainer Contact -========================================================================= -fhem.pl rudolfkoenig http://forum.fhem.de Sonstiges -fhem.cfg rudolfkoenig http://forum.fhem.de Sonstiges -Makefile rudolfkoenig http://forum.fhem.de Sonstiges -configDB.pm betateilchen http://forum.fhem.de Sonstiges - -FHEM/00_CM11.pm borisneubert http://forum.fhem.de SlowRF -FHEM/00_CUL.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/00_FBAHA.pm rudolfkoenig http://forum.fhem.de FRITZ!Box -FHEM/00_FHZ.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/00_HMLAN.pm martinp876 http://forum.fhem.de HomeMatic -FHEM/00_KM271.pm physikus http://forum.fhem.de Sonstiges -FHEM/00_LIRC.pm rudolfkoenig http://forum.fhem.de Sonstiges -FHEM/00_MAXLAN.pm mgehre http://forum.fhem.de MAX -FHEM/00_MQTT.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/00_MYSENSORS.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/00_NetzerI2C.pm klausw http://forum.fhem.de Sonstige Systeme -FHEM/00_OWX.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/00_OWX_ASYNC ntruchsess http://forum.fhem.de 1Wire -FHEM/00_RPII2C klausw http://forum.fhem.de Einplatinencomputer -FHEM/00_TCM.pm klaus-schauer http://forum.fhem.de EnOcean -FHEM/00_THZ.pm immiimmi http://forum.fhem.de Sonstiges -FHEM/00_TUL.pm hotmaz http://forum.fhem.de KNX/EIB -FHEM/00_ZWDongle.pm rudolfkoenig http://forum.fhem.de ZWave -FHEM/01_FHEMWEB.pm rudolfkoenig http://forum.fhem.de Frontends -FHEM/02_FRAMEBUFFER.pm kaihs http://forum.fhem.de Frontends -FHEM/02_HTTPSRV.pm borisneubert http://forum.fhem.de Frontends -FHEM/02_RSS.pm borisneubert http://forum.fhem.de Frontends -FHEM/09_BS.pm borisneubert http://forum.fhem.de SlowRF -FHEM/09_CUL_FHTTK.pm matscher http://forum.fhem.de SlowRF -FHEM/09_USF1000.pm borisneubert http://forum.fhem.de SlowRF -FHEM/10_CUL_HM.pm martinp876 http://forum.fhem.de HomeMatic -FHEM/10_CUL_IR.pm odroegehorn http://forum.fhem.de SlowRF -FHEM/10_EIB.pm hotmaz http://forum.fhem.de KNX/EIB -FHEM/10_EnOcean.pm klaus-schauer http://forum.fhem.de EnOcean -FHEM/10_FBDECT.pm rudolfkoenig http://forum.fhem.de FRITZ!Box -FHEM/10_FRM.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/10_FS20.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/10_IT.pm justme1968 http://forum.fhem.de InterTechno -FHEM/10_Itach_IR ulimaass http://forum.fhem.de Sonstige Systeme -FHEM/10_MAX.pm mgehre http://forum.fhem.de MAX -FHEM/10_MQTT_BRIDGE ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/10_MQTT_DEVICE ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/10_MYSENSORS_DEVICE ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/10_OWServer.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire -FHEM/10_SOMFY.pm thdankert http://forum.fhem.de Sonstiges -FHEM/10_UNIRoll.pm c-herrmann http://forum.fhem.de SlowRF -FHEM/10_ZWave.pm rudolfkoenig http://forum.fhem.de ZWave -FHEM/10_RESIDENTS.pm loredo http://forum.fhem.de Automatisierung -FHEM/11_FHT.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/11_FHT8V.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/11_OWDevice.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire -FHEM/12_HMS.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/13_KS300.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/14_CUL_MAX.pm mgehre http://forum.fhem.de MAX -FHEM/14_CUL_TX.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/14_CUL_WS.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/15_CUL_EM.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/16_CUL_RFR.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/16_STACKABLE_CC.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/17_EGPM2LAN.pm alexus http://forum.fhem.de Sonstiges -FHEM/17_SIS_PMS.pm painseeker http://forum.fhem.de Sonstiges -FHEM/18_CUL_HOERMANN.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/19_Revolt.pm martinppp/mehf http://forum.fhem.de SlowRF -FHEM/20_FRM_AD.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_ROTENC.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_I2C.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_IN.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_LCD.pm ntruchsess http://forum.fhem.de Sonstige Systeme (deprecated) -FHEM/20_FRM_OUT.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_PWM.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_RBG.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_SERVO.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_FRM_STEPPER.pm ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/20_OWFS.pm mfr69bs http://forum.fhem.de 1Wire (deprecated) -FHEM/20_X10.pm borisneubert http://forum.fhem.de SlowRF -FHEM/20_ROOMMATE.pm loredo http://forum.fhem.de Automatisierung -FHEM/20_GUEST.pm loredo http://forum.fhem.de Automatisierung -FHEM/21_OWAD.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWCOUNT.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWID.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWLCD.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWMULTI.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWSWITCH.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/21_OWTEMP.pm mfr69bs http://forum.fhem.de 1Wire (deprecated) -FHEM/21_OWTHERM.pm pahenning/ntruchsess http://forum.fhem.de 1Wire -FHEM/22_ALL3076.pm sachag http://forum.fhem.de Snstiges -FHEM/23_ALL4027.pm sachag http://forum.fhem.de Sonstiges -FHEM/23_KOSTALPIKO.pm john http://forum.fhem.de CodeSchnipsel -FHEM/23_LUXTRONIK2.pm tupol http://forum.fhem.de Sonstiges (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) -FHEM/23_WEBIO.pm sachag http://forum.fhem.de Sonstiges -FHEM/23_WEBIO_12DIGITAL.pm sachag http://forum.fhem.de Sonstiges -FHEM/24_NetIO230B.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges -FHEM/30_HUEBridge.pm justme1968 http://forum.fhem.de Beleuchtung -FHEM/30_ENECSYSGW.pm akw http://forum.fhem.de Sonstige Systeme -FHEM/31_HUEDevice.pm justme1968 http://forum.fhem.de Beleuchtung -FHEM/31_ENECSYSINV.pm akw http://forum.fhem.de Sonstige Systeme -FHEM/31_LightScene.pm justme1968 http://forum.fhem.de Automatisierung -FHEM/32_SYSSTAT.pm justme1968 http://forum.fhem.de Unterstuetzende Dienste -FHEM/32_mailcheck.pm justme1968 http://forum.fhem.de Automatisierung -FHEM/32_withings.pm justme1968 http://forum.fhem.de Sonstiges -FHEM/33_readingsGroup.pm justme1968 http://forum.fhem.de Frontends -FHEM/33_readingsHistory.pm justme1968 http://forum.fhem.de Frontends -FHEM/33_readingsProxy.pm justme1968 http://forum.fhem.de Automatisierung -FHEM/32_speedtest.pm justme1968 http://forum.fhem.de Sonstiges -FHEM/34_NUT.pm creideiki http://forum.fhem.de Sonstige Systeme -FHEM/34_panStamp.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/34_SWAP.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/35_SWAP_0000002200000003.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/35_SWAP_0000002200000008.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/36_EC3000.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/36_JeeLink.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/36_PCA301.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/36_LaCrosse.pm justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/36_EMT7110.pm HCS http://forum.fhem.de Sonstige Systeme -FHEM/36_Level.pm HCS http://forum.fhem.de Sonstige Systeme -FHEM/36_WMBUS.pm kaihs http://forum.fhem.de Sonstige Systeme -FHEM/37_SHC.pm rr2000 http://forum.fhem.de Sonstige Systeme -FHEM/37_SHCdev.pm rr2000 http://forum.fhem.de Sonstige Systeme -FHEM/38_harmony.pm justme1968 http://forum.fhem.de Multimedia -FHEM/38_CO20.pm justme1968 http://forum.fhem.de Sonstiges -FHEM/40_RFXCOM.pm wherzig http://forum.fhem.de RFXTRX -FHEM/41_OREGON.pm wherzig http://forum.fhem.de Sonstiges -FHEM/42_RFXMETER.pm wherzig http://forum.fhem.de RFXTRX +maintainer of the file to do the change. + +The third column specifies, where/how the maintainer should be contacted. If +there is no reaction from the mainainer within 3 weeks, then rudolfkoenig +(forum.fhem.de/FHEM Forum) should be contacted, in order to assign a new +maintainer. + +When adding a new file, add yourself as the maintainer. + +File Maintainer Contact +========================================================================= +fhem.pl rudolfkoenig http://forum.fhem.de Sonstiges +fhem.cfg rudolfkoenig http://forum.fhem.de Sonstiges +Makefile rudolfkoenig http://forum.fhem.de Sonstiges +configDB.pm betateilchen http://forum.fhem.de Sonstiges + +FHEM/00_CM11.pm borisneubert http://forum.fhem.de SlowRF +FHEM/00_CUL.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/00_FBAHA.pm rudolfkoenig http://forum.fhem.de FRITZ!Box +FHEM/00_FHZ.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/00_HMLAN.pm martinp876 http://forum.fhem.de HomeMatic +FHEM/00_KM271.pm physikus http://forum.fhem.de Sonstiges +FHEM/00_LIRC.pm rudolfkoenig http://forum.fhem.de Sonstiges +FHEM/00_MAXLAN.pm mgehre http://forum.fhem.de MAX +FHEM/00_MQTT.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/00_MYSENSORS.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/00_NetzerI2C.pm klausw http://forum.fhem.de Sonstige Systeme +FHEM/00_OWX.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/00_OWX_ASYNC ntruchsess http://forum.fhem.de 1Wire +FHEM/00_RPII2C klausw http://forum.fhem.de Einplatinencomputer +FHEM/00_SONOS.pm Reinerlein http://forum.fhem.de Multimedia +FHEM/00_TCM.pm klaus-schauer http://forum.fhem.de EnOcean +FHEM/00_THZ.pm immiimmi http://forum.fhem.de Sonstiges +FHEM/00_TUL.pm hotmaz http://forum.fhem.de KNX/EIB +FHEM/00_ZWDongle.pm rudolfkoenig http://forum.fhem.de ZWave +FHEM/01_FHEMWEB.pm rudolfkoenig http://forum.fhem.de Frontends +FHEM/02_FRAMEBUFFER.pm kaihs http://forum.fhem.de Frontends +FHEM/02_HTTPSRV.pm borisneubert http://forum.fhem.de Frontends +FHEM/02_RSS.pm borisneubert http://forum.fhem.de Frontends +FHEM/09_BS.pm borisneubert http://forum.fhem.de SlowRF +FHEM/09_CUL_FHTTK.pm matscher http://forum.fhem.de SlowRF +FHEM/09_USF1000.pm borisneubert http://forum.fhem.de SlowRF +FHEM/10_CUL_HM.pm martinp876 http://forum.fhem.de HomeMatic +FHEM/10_CUL_IR.pm odroegehorn http://forum.fhem.de SlowRF +FHEM/10_EIB.pm hotmaz http://forum.fhem.de KNX/EIB +FHEM/10_EnOcean.pm klaus-schauer http://forum.fhem.de EnOcean +FHEM/10_FBDECT.pm rudolfkoenig http://forum.fhem.de FRITZ!Box +FHEM/10_FRM.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/10_FS20.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/10_IT.pm justme1968 http://forum.fhem.de InterTechno +FHEM/10_Itach_IR ulimaass http://forum.fhem.de Sonstige Systeme +FHEM/10_MAX.pm mgehre http://forum.fhem.de MAX +FHEM/10_MQTT_BRIDGE ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/10_MQTT_DEVICE ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/10_MYSENSORS_DEVICE ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/10_OWServer.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire +FHEM/10_SOMFY.pm thdankert http://forum.fhem.de Sonstiges +FHEM/10_UNIRoll.pm c-herrmann http://forum.fhem.de SlowRF +FHEM/10_ZWave.pm rudolfkoenig http://forum.fhem.de ZWave +FHEM/10_RESIDENTS.pm loredo http://forum.fhem.de Automatisierung +FHEM/11_FHT.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/11_FHT8V.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/11_OWDevice.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire +FHEM/12_HMS.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/13_KS300.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/14_CUL_MAX.pm mgehre http://forum.fhem.de MAX +FHEM/14_CUL_TX.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/14_CUL_WS.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/15_CUL_EM.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/16_CUL_RFR.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/16_STACKABLE_CC.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/17_EGPM2LAN.pm alexus http://forum.fhem.de Sonstiges +FHEM/17_SIS_PMS.pm painseeker http://forum.fhem.de Sonstiges +FHEM/18_CUL_HOERMANN.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/19_Revolt.pm martinppp/mehf http://forum.fhem.de SlowRF +FHEM/20_FRM_AD.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_ROTENC.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_I2C.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_IN.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_LCD.pm ntruchsess http://forum.fhem.de Sonstige Systeme (deprecated) +FHEM/20_FRM_OUT.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_PWM.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_RBG.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_SERVO.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_FRM_STEPPER.pm ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/20_OWFS.pm mfr69bs http://forum.fhem.de 1Wire (deprecated) +FHEM/20_X10.pm borisneubert http://forum.fhem.de SlowRF +FHEM/20_ROOMMATE.pm loredo http://forum.fhem.de Automatisierung +FHEM/20_GUEST.pm loredo http://forum.fhem.de Automatisierung +FHEM/21_OWAD.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWCOUNT.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWID.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWLCD.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWMULTI.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWSWITCH.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_OWTEMP.pm mfr69bs http://forum.fhem.de 1Wire (deprecated) +FHEM/21_OWTHERM.pm pahenning/ntruchsess http://forum.fhem.de 1Wire +FHEM/21_SONOSPLAYER Reinerlein http://forum.fhem.de Multimedia +FHEM/22_ALL3076.pm sachag http://forum.fhem.de Snstiges +FHEM/23_ALL4027.pm sachag http://forum.fhem.de Sonstiges +FHEM/23_KOSTALPIKO.pm john http://forum.fhem.de CodeSchnipsel +FHEM/23_LUXTRONIK2.pm tupol http://forum.fhem.de Sonstiges (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) +FHEM/23_WEBIO.pm sachag http://forum.fhem.de Sonstiges +FHEM/23_WEBIO_12DIGITAL.pm sachag http://forum.fhem.de Sonstiges +FHEM/24_NetIO230B.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges +FHEM/30_HUEBridge.pm justme1968 http://forum.fhem.de Beleuchtung +FHEM/30_ENECSYSGW.pm akw http://forum.fhem.de Sonstige Systeme +FHEM/31_HUEDevice.pm justme1968 http://forum.fhem.de Beleuchtung +FHEM/31_ENECSYSINV.pm akw http://forum.fhem.de Sonstige Systeme +FHEM/31_LightScene.pm justme1968 http://forum.fhem.de Automatisierung +FHEM/32_SYSSTAT.pm justme1968 http://forum.fhem.de Unterstuetzende Dienste +FHEM/32_mailcheck.pm justme1968 http://forum.fhem.de Automatisierung +FHEM/32_withings.pm justme1968 http://forum.fhem.de Sonstiges +FHEM/33_readingsGroup.pm justme1968 http://forum.fhem.de Frontends +FHEM/33_readingsHistory.pm justme1968 http://forum.fhem.de Frontends +FHEM/33_readingsProxy.pm justme1968 http://forum.fhem.de Automatisierung +FHEM/32_speedtest.pm justme1968 http://forum.fhem.de Sonstiges +FHEM/34_NUT.pm creideiki http://forum.fhem.de Sonstige Systeme +FHEM/34_panStamp.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/34_SWAP.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/35_SWAP_0000002200000003.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/35_SWAP_0000002200000008.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/36_EC3000.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/36_JeeLink.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/36_PCA301.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/36_LaCrosse.pm justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/36_EMT7110.pm HCS http://forum.fhem.de Sonstige Systeme +FHEM/36_Level.pm HCS http://forum.fhem.de Sonstige Systeme +FHEM/36_WMBUS.pm kaihs http://forum.fhem.de Sonstige Systeme +FHEM/37_SHC.pm rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/37_SHCdev.pm rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/38_harmony.pm justme1968 http://forum.fhem.de Multimedia +FHEM/38_CO20.pm justme1968 http://forum.fhem.de Sonstiges +FHEM/40_RFXCOM.pm wherzig http://forum.fhem.de RFXTRX +FHEM/41_OREGON.pm wherzig http://forum.fhem.de Sonstiges +FHEM/42_RFXMETER.pm wherzig http://forum.fhem.de RFXTRX FHEM/42_SMARTMON.pm hexenmeister http://forum.fhem.de Unterstuetzende Dienste -FHEM/42_SYSMON.pm hexenmeister http://forum.fhem.de Unterstuetzende Dienste -FHEM/43_RFXX10REC.pm wherzig http://forum.fhem.de RFXTRX -FHEM/44_TEK603.pm eisler http://forum.fhem.de Sonstige Systeme -FHEM/45_TRX.pm wherzig http://forum.fhem.de RFXTRX -FHEM/46_TRX_ELSE.pm wherzig http://forum.fhem.de RFXTRX -FHEM/46_TRX_LIGHT.pm wherzig http://forum.fhem.de RFXTRX -FHEM/46_TRX_SECURITY.pm wherzig http://forum.fhem.de RFXTRX -FHEM/46_TRX_WEATHER.pm wherzig http://forum.fhem.de RFXTRX -FHEM/49_IPCAM.pm mfr69bs http://forum.fhem.de Sonstiges -FHEM/50_WS300.pm Dirk http://forum.fhem.de SlowRF -FHEM/51_I2C_BMP180.pm Dirk http://forum.fhem.de Einplatinencomputer -FHEM/51_Netzer.pm klausw http://forum.fhem.de Sonstige Systeme -FHEM/51_RPI_GPIO.pm klausw http://forum.fhem.de Einplatinencomputer -FHEM/52_I2C_DS1307 ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_EEPROM.pm klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_LCD ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_MCP23008 klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_MCP23017 klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_MCP342x klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_PCA9532 klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_PCF8574 klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_SHT21 klausw http://forum.fhem.de Sonstige Systeme -FHEM/52_I2C_TSL2561 kaihs http://forum.fhem.de Sonstige Systeme -FHEM/55_GDS.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste -FHEM/55_PIFACE.pm klaus.schauer http://forum.fhem.de Einplatinencomputer -FHEM/55_weco.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste -FHEM/56_POKEYS.pm axelberner http://forum.fhem.de Sonstiges -FHEM/57_Calendar.pm borisneubert http://forum.fhem.de Unterstützende Dienste -FHEM/59_HCS.pm mfr69bs http://forum.fhem.de Automatisierung -FHEM/59_OPENWEATHER.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) -FHEM/59_Twilight.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste -FHEM/59_PROPLANTA.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) -FHEM/59_WWO.pm baumrasen http://forum.fhem.de Sonstiges -FHEM/59_Weather.pm borisneubert http://forum.fhem.de Unterstützende Dienste -FHEM/60_EM.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/61_EMWZ.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/62_EMEM.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/63_EMGZ.pm rudolfkoenig http://forum.fhem.de SlowRF -FHEM/64_ESA2000.pm stromer-12 http://forum.fhem.de SlowRF -FHEM/66_ECMD.pm borisneubert http://forum.fhem.de Sonstiges -FHEM/67_ECMDDevice.pm borisneubert http://forum.fhem.de Sonstiges -FHEM/70_EGPM.pm alexus http://forum.fhem.de Sonstiges -FHEM/70_ENIGMA2.pm loredo http://forum.fhem.de Multimedia -FHEM/70_Jabber.pm BioS http://forum.fhem.de Unterstuetzende Dienste -FHEM/70_JSONMETER.pm tupol http://forum.fhem.de Sonstiges -FHEM/70_PHTV.pm loredo http://forum.fhem.de Multimedia -FHEM/70_ONKYO_AVR.pm loredo http://forum.fhem.de Multimedia -FHEM/70_PIONEERAVR.pm hofrichter http://forum.fhem.de Multimedia -FHEM/70_SCIVT.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges -FHEM/70_SISPM.pm real-wusel http://forum.fhem.de Sonstiges -FHEM/70_SML.pm bentele http://forum.fhem.de Sonstiges -FHEM/70_STV.pm bentele http://forum.fhem.de Sonstiges -FHEM/70_TellStick.pm real-wusel http://forum.fhem.de Sonstiges -FHEM/70_USBWX.pm wherzig http://forum.fhem.de Sonstiges -FHEM/70_VIERA.pm teevau http://forum.fhem.de Sonstiges -FHEM/70_WS3600.pm Josch http://forum.fhem.de Sonstiges -FHEM/70_XBMC.pm dennisb http://forum.fhem.de Multimedia -FHEM/70_Pushover.pm Johannes_B http://forum.fhem.de Unterstuetzende Dienste -FHEM/70_PushNotifier.pm xusader http://forum.fhem.de Unterstuetzende Dienste -FHEM/71_YAMAHA_AVR.pm markusbloch http://forum.fhem.de Multimedia -FHEM/71_YAMAHA_BD.pm markusbloch http://forum.fhem.de Multimedia -FHEM/72_FB_CALLMONITOR.pm markusbloch http://forum.fhem.de Unterstuetzende Dienste -FHEM/72_FRITZBOX.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) -FHEM/73_PRESENCE.pm markusbloch http://forum.fhem.de Unterstuetzende Dienste -FHEM/73_MPD.pm wzut http://forum.fhem.de Multimedia -FHEM/75_MSG.pm ruebedo http://forum.fhem.de Automatisierung -FHEM/76_MSGFile.pm ruebedo http://forum.fhem.de Automatisierung -FHEM/76_MSGMail.pm ruebedo http://forum.fhem.de Automatisierung -FHEM/80_M232.pm borisneubert http://forum.fhem.de Sonstiges -FHEM/80_xxLG7000.pm painseeker http://forum.fhem.de Sonstiges -FHEM/81_M232Counter.pm borisneubert http://forum.fhem.de Sonstiges -FHEM/82_LGTV.pm painseeker http://forum.fhem.de Sonstiges -FHEM/82_M232Voltage.pm borisneubert http://forum.fhem.de Sonstiges -FHEM/87_WS2000.pm tdressler http://forum.fhem.de Sonstiges -FHEM/88_ALL4000T.pm sachag http://forum.fhem.de Sonstiges -FHEM/88_LINDY_HDMI_SWITCH.pm sachag http://forum.fhem.de Multimedia -FHEM/88_IPWE.pm tdressler http://forum.fhem.de Sonstiges -FHEM/88_Itach_Relay.pm sachag http://forum.fhem.de Automatisierung -FHEM/88_Itach_IRDevice ulimaass http://forum.fhem.de Sonstige Systeme -FHEM/88_VantagePro2.pm sachag http://forum.fhem.de Sonstiges -FHEM/88_WEBCOUNT.pm sachag http://forum.fhem.de Sonstiges -FHEM/89_HEATRONIC.pm heikoranft http://forum.fhem.de Sonstige Systeme -FHEM/89_VCONTROL.pm adamwit http://forum.fhem.de Heizungssteuerung/Raumklima -FHEM/90_at.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/91_eventTypes.pm rudolfkoenig http://forum.fhem.de Frontends -FHEM/91_notify.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/91_sequence.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/91_watchdog.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/92_FileLog.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/93_DbLog.pm tobiasfaust http://forum.fhem.de Automatisierung -FHEM/93_FHEM2FHEM.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/95_FLOORPLAN.pm ulimaass http://forum.fhem.de Frontends -FHEM/95_Dashboard.pm svenson08 http://forum.fhem.de Frontends -FHEM/95_PachLog.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges -FHEM/95_holiday.pm rudolfkoenig http://forum.fhem.de Sonstiges -FHEM/95_remotecontrol.pm ulimaass http://forum.fhem.de Frontends -FHEM/98_Text2Speech.pm tobiasfaust http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_apptime.pm martinp876 http://forum.fhem.de Sonstiges -FHEM/98_ComfoAir.pm StefanStrobel http://forum.fhem.de Sonstiges -FHEM/98_CULflash.pm rudolfkoenig http://forum.fhem.de Sonstiges -FHEM/98_DOIF.pm damian-s http://forum.fhem.de Automatisierung -FHEM/98_FReplacer.pm stefanstrobel http://forum.fhem.de Sonstiges -FHEM/98_GEOFANCY.pm loredo http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_HMinfo.pm martinp876 http://forum.fhem.de HomeMatic -FHEM/98_Heating_Control.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_HTTPMOD.pm stefanstrobel http://forum.fhem.de Sonstiges -FHEM/98_IF.pm damian-s http://forum.fhem.de Automatisierung -FHEM/98_JsonList.pm mfr69bs http://forum.fhem.de Automatisierung -FHEM/98_JsonList2.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_PID20.pm John http://forum.fhem.de Automatisierung -FHEM/98_RandomTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_SVG.pm rudolfkoenig http://forum.fhem.de Frontends -FHEM/98_THRESHOLD.pm damian-s http://forum.fhem.de Automatisierung -FHEM/98_WeekdayTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_WOL.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_XmlList.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_autocreate.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_average.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_backup.pm rudlfkoenig http://forum.fhem.de Sonstiges -FHEM/98_cloneDummy.pm Joachim http://forum.fhem.de Automatisierung -FHEM/98_cmdalias.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_configdb.pm betateilchen http://forum.fhem.de Sonstiges -FHEM/98_copy.pm justme1968 http://forum.fhem.de Sonstiges -FHEM/98_CustomReadings.pm HCS http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_dewpoint.pm Joachim http://forum.fhem.de Automatisierung -FHEM/98_dummy.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_fheminfo.pm mfr69bs http://forum.fhem.de Sonstiges -FHEM/98_HourCounter.pm john http://forum.fhem.de MAX -FHEM/98_logProxy.pm justme1968 http://forum.fhem.de Frontends -FHEM/98_notice.pm mfr69bs http://forum.fhem.de Sonstiges -FHEM/98_pilight.pm andreas-fey http://forum.fhem.de Unterstuetzende Dienste -FHEM/98_rain.pm baumrasen http://forum.fhem.de Sonstiges -FHEM/98_restore.pm rudolgkoenig http://forum.fhem.de Sonstiges -FHEM/98_statistics.pm tupol http://forum.fhem.de Sonstiges (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) -FHEM/98_structure.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_telnet.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/98_update.pm rudolgkoenig http://forum.fhem.de Sonstiges -FHEM/98_weblink.pm rudolfkoenig http://forum.fhem.de Frontends -FHEM/99_SUNRISE_EL.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/99_Utils.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/Blocking.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/DevIo.pm rudolfkoenig http://forum.fhem.de Sonstiges -FHEM/Color.pm justme1968 http://forum.fhem.de Sonstiges -FHEM/FritzBoxUtils.pm rudolfkoenig http://forum.fhem.de FRITZ!Box -FHEM/HMConfig.pm martinp876 http://forum.fhem.de HomeMatic -FHEM/HttpUtils.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/MaxCommon.pm mgehre http://forum.fhem.de MAX -FHEM/ONKYOdb.pm loredo http://forum.fhem.de Multimedia -FHEM/OWX_DS2480.pm ntruchsess http://forum.fhem.de 1Wire -FHEM/OWX_DS9097.pm ntruchsess http://forum.fhem.de 1Wire -FHEM/OWX_FRM.pm ntruchsess http://forum.fhem.de 1Wire -FHEM/OWX_SER.pm ntruchsess http://forum.fhem.de 1Wire -FHEM/SetExtensions.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/SHC_datafields.pm rr2000 http://forum.fhem.de Sonstige Systeme -FHEM/SHC_parser.pm rr2000 http://forum.fhem.de Sonstige Systeme -FHEM/TcpServerUtils.pm rudolfkoenig http://forum.fhem.de Automatisierung -FHEM/lib/Device/Firmata/* ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/lib/Device/MySensors/* ntruchsess http://forum.fhem.de Sonstige Systeme -FHEM/lib/ProtoThreads.pm ntruchsess http://forum.fhem.de FHEM Development -FHEM/lib/SHC_packet_layout.xml rr2000 http://forum.fhem.de Sonstige Systeme -FHEM/lib/SWAP/* justme1968 http://forum.fhem.de Sonstige Systeme -FHEM/FhemUtils/* mfr69bs http://forum.fhem.de Sonstiges -FHEM/GPUtils.pm ntruchsess http://forum.fhem.de FHEM Development - -contrib/YAF/* danielweisensee http://forum.fhem.de Frontends -contrib/23_WEBTHERM.pm betateilchen/sachag http://forum.fhem.de Sonstiges -contrib/55_BBB_BMP180.pm betateilchen http://forum.fhem.de Einplatinencomputer -contrib/71_LISTENLIVE.pm betateilchen http://forum.fhem.de Multimedia -contrib/98_geodata.pm betateilchen http://forum.fhem.de Sonstiges -contrib/98_openweathermap.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste -contrib/98_PID.pm betateilchen http://forum.fhem.de Automatisierung - -www/codemirror/* betateilchen http://forum.fhem.de Frontends -www/gplot/* rudolfkoenig http://forum.fhem.de Frontends -www/images/* ulimaass http://forum.fhem.de Frontends -www/pgm2/dashboard/* svenson08 http://forum.fhem.de Frontends -www/pgm2/fhemweb_readingsHistory.js justme1968 http://forum.fhem.de Frontends -www/pgm2/* rudolfkoenig http://forum.fhem.de Frontends -www/jscolor/* justme1968 http://forum.fhem.de Frontends -www/frontend/* johannnes http://forum.fhem.de Frontends - -docs/fhem-floorplan-* ulimaass http://forum.fhem.de Sonstiges -docs/* rudolfkoenig http://forum.fhem.de Sonstiges - -Files that every developer should modify/extend - MAINTAINER.txt - CHANGES - HISTORY +FHEM/42_SYSMON.pm hexenmeister http://forum.fhem.de Unterstuetzende Dienste +FHEM/43_RFXX10REC.pm wherzig http://forum.fhem.de RFXTRX +FHEM/44_TEK603.pm eisler http://forum.fhem.de Sonstige Systeme +FHEM/45_TRX.pm wherzig http://forum.fhem.de RFXTRX +FHEM/46_TRX_ELSE.pm wherzig http://forum.fhem.de RFXTRX +FHEM/46_TRX_LIGHT.pm wherzig http://forum.fhem.de RFXTRX +FHEM/46_TRX_SECURITY.pm wherzig http://forum.fhem.de RFXTRX +FHEM/46_TRX_WEATHER.pm wherzig http://forum.fhem.de RFXTRX +FHEM/49_IPCAM.pm mfr69bs http://forum.fhem.de Sonstiges +FHEM/50_WS300.pm Dirk http://forum.fhem.de SlowRF +FHEM/51_I2C_BMP180.pm Dirk http://forum.fhem.de Einplatinencomputer +FHEM/51_Netzer.pm klausw http://forum.fhem.de Sonstige Systeme +FHEM/51_RPI_GPIO.pm klausw http://forum.fhem.de Einplatinencomputer +FHEM/52_I2C_DS1307 ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_EEPROM.pm klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_LCD ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_MCP23008 klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_MCP23017 klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_MCP342x klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_PCA9532 klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_PCF8574 klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_SHT21 klausw http://forum.fhem.de Sonstige Systeme +FHEM/52_I2C_TSL2561 kaihs http://forum.fhem.de Sonstige Systeme +FHEM/55_GDS.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste +FHEM/55_PIFACE.pm klaus.schauer http://forum.fhem.de Einplatinencomputer +FHEM/55_weco.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste +FHEM/56_POKEYS.pm axelberner http://forum.fhem.de Sonstiges +FHEM/57_Calendar.pm borisneubert http://forum.fhem.de Unterstützende Dienste +FHEM/59_HCS.pm mfr69bs http://forum.fhem.de Automatisierung +FHEM/59_OPENWEATHER.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) +FHEM/59_Twilight.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste +FHEM/59_PROPLANTA.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) +FHEM/59_WWO.pm baumrasen http://forum.fhem.de Sonstiges +FHEM/59_Weather.pm borisneubert http://forum.fhem.de Unterstützende Dienste +FHEM/60_EM.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/61_EMWZ.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/62_EMEM.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/63_EMGZ.pm rudolfkoenig http://forum.fhem.de SlowRF +FHEM/64_ESA2000.pm stromer-12 http://forum.fhem.de SlowRF +FHEM/66_ECMD.pm borisneubert http://forum.fhem.de Sonstiges +FHEM/67_ECMDDevice.pm borisneubert http://forum.fhem.de Sonstiges +FHEM/70_EGPM.pm alexus http://forum.fhem.de Sonstiges +FHEM/70_ENIGMA2.pm loredo http://forum.fhem.de Multimedia +FHEM/70_Jabber.pm BioS http://forum.fhem.de Unterstuetzende Dienste +FHEM/70_JSONMETER.pm tupol http://forum.fhem.de Sonstiges +FHEM/70_PHTV.pm loredo http://forum.fhem.de Multimedia +FHEM/70_ONKYO_AVR.pm loredo http://forum.fhem.de Multimedia +FHEM/70_PIONEERAVR.pm hofrichter http://forum.fhem.de Multimedia +FHEM/70_SCIVT.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges +FHEM/70_SISPM.pm real-wusel http://forum.fhem.de Sonstiges +FHEM/70_SML.pm bentele http://forum.fhem.de Sonstiges +FHEM/70_STV.pm bentele http://forum.fhem.de Sonstiges +FHEM/70_TellStick.pm real-wusel http://forum.fhem.de Sonstiges +FHEM/70_USBWX.pm wherzig http://forum.fhem.de Sonstiges +FHEM/70_VIERA.pm teevau http://forum.fhem.de Sonstiges +FHEM/70_WS3600.pm Josch http://forum.fhem.de Sonstiges +FHEM/70_XBMC.pm dennisb http://forum.fhem.de Multimedia +FHEM/70_Pushover.pm Johannes_B http://forum.fhem.de Unterstuetzende Dienste +FHEM/70_PushNotifier.pm xusader http://forum.fhem.de Unterstuetzende Dienste +FHEM/71_YAMAHA_AVR.pm markusbloch http://forum.fhem.de Multimedia +FHEM/71_YAMAHA_BD.pm markusbloch http://forum.fhem.de Multimedia +FHEM/72_FB_CALLMONITOR.pm markusbloch http://forum.fhem.de Unterstuetzende Dienste +FHEM/72_FRITZBOX.pm tupol http://forum.fhem.de Unterstuetzende Dienste (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) +FHEM/73_PRESENCE.pm markusbloch http://forum.fhem.de Unterstuetzende Dienste +FHEM/73_MPD.pm wzut http://forum.fhem.de Multimedia +FHEM/75_MSG.pm ruebedo http://forum.fhem.de Automatisierung +FHEM/76_MSGFile.pm ruebedo http://forum.fhem.de Automatisierung +FHEM/76_MSGMail.pm ruebedo http://forum.fhem.de Automatisierung +FHEM/80_M232.pm borisneubert http://forum.fhem.de Sonstiges +FHEM/80_xxLG7000.pm painseeker http://forum.fhem.de Sonstiges +FHEM/81_M232Counter.pm borisneubert http://forum.fhem.de Sonstiges +FHEM/82_LGTV.pm painseeker http://forum.fhem.de Sonstiges +FHEM/82_M232Voltage.pm borisneubert http://forum.fhem.de Sonstiges +FHEM/87_WS2000.pm tdressler http://forum.fhem.de Sonstiges +FHEM/88_ALL4000T.pm sachag http://forum.fhem.de Sonstiges +FHEM/88_LINDY_HDMI_SWITCH.pm sachag http://forum.fhem.de Multimedia +FHEM/88_IPWE.pm tdressler http://forum.fhem.de Sonstiges +FHEM/88_Itach_Relay.pm sachag http://forum.fhem.de Automatisierung +FHEM/88_Itach_IRDevice ulimaass http://forum.fhem.de Sonstige Systeme +FHEM/88_VantagePro2.pm sachag http://forum.fhem.de Sonstiges +FHEM/88_WEBCOUNT.pm sachag http://forum.fhem.de Sonstiges +FHEM/89_HEATRONIC.pm heikoranft http://forum.fhem.de Sonstige Systeme +FHEM/89_VCONTROL.pm adamwit http://forum.fhem.de Heizungssteuerung/Raumklima +FHEM/90_at.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/91_eventTypes.pm rudolfkoenig http://forum.fhem.de Frontends +FHEM/91_notify.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/91_sequence.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/91_watchdog.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/92_FileLog.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/93_DbLog.pm tobiasfaust http://forum.fhem.de Automatisierung +FHEM/93_FHEM2FHEM.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/95_FLOORPLAN.pm ulimaass http://forum.fhem.de Frontends +FHEM/95_Dashboard.pm svenson08 http://forum.fhem.de Frontends +FHEM/95_PachLog.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges +FHEM/95_holiday.pm rudolfkoenig http://forum.fhem.de Sonstiges +FHEM/95_remotecontrol.pm ulimaass http://forum.fhem.de Frontends +FHEM/98_Text2Speech.pm tobiasfaust http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_apptime.pm martinp876 http://forum.fhem.de Sonstiges +FHEM/98_ComfoAir.pm StefanStrobel http://forum.fhem.de Sonstiges +FHEM/98_CULflash.pm rudolfkoenig http://forum.fhem.de Sonstiges +FHEM/98_DOIF.pm damian-s http://forum.fhem.de Automatisierung +FHEM/98_FReplacer.pm stefanstrobel http://forum.fhem.de Sonstiges +FHEM/98_GEOFANCY.pm loredo http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_HMinfo.pm martinp876 http://forum.fhem.de HomeMatic +FHEM/98_Heating_Control.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_HTTPMOD.pm stefanstrobel http://forum.fhem.de Sonstiges +FHEM/98_IF.pm damian-s http://forum.fhem.de Automatisierung +FHEM/98_JsonList.pm mfr69bs http://forum.fhem.de Automatisierung +FHEM/98_JsonList2.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_PID20.pm John http://forum.fhem.de Automatisierung +FHEM/98_RandomTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_SVG.pm rudolfkoenig http://forum.fhem.de Frontends +FHEM/98_THRESHOLD.pm damian-s http://forum.fhem.de Automatisierung +FHEM/98_WeekdayTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_WOL.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_XmlList.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_autocreate.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_average.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_backup.pm rudlfkoenig http://forum.fhem.de Sonstiges +FHEM/98_cloneDummy.pm Joachim http://forum.fhem.de Automatisierung +FHEM/98_cmdalias.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_configdb.pm betateilchen http://forum.fhem.de Sonstiges +FHEM/98_copy.pm justme1968 http://forum.fhem.de Sonstiges +FHEM/98_CustomReadings.pm HCS http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_dewpoint.pm Joachim http://forum.fhem.de Automatisierung +FHEM/98_dummy.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_fheminfo.pm mfr69bs http://forum.fhem.de Sonstiges +FHEM/98_HourCounter.pm john http://forum.fhem.de MAX +FHEM/98_logProxy.pm justme1968 http://forum.fhem.de Frontends +FHEM/98_notice.pm mfr69bs http://forum.fhem.de Sonstiges +FHEM/98_pilight.pm andreas-fey http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_rain.pm baumrasen http://forum.fhem.de Sonstiges +FHEM/98_restore.pm rudolgkoenig http://forum.fhem.de Sonstiges +FHEM/98_statistics.pm tupol http://forum.fhem.de Sonstiges (PM: http://forum.fhem.de/index.php?action=pm;sa=send;u=5432) +FHEM/98_structure.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_telnet.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_update.pm rudolgkoenig http://forum.fhem.de Sonstiges +FHEM/98_weblink.pm rudolfkoenig http://forum.fhem.de Frontends +FHEM/99_SUNRISE_EL.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/99_Utils.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/Blocking.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/DevIo.pm rudolfkoenig http://forum.fhem.de Sonstiges +FHEM/Color.pm justme1968 http://forum.fhem.de Sonstiges +FHEM/FritzBoxUtils.pm rudolfkoenig http://forum.fhem.de FRITZ!Box +FHEM/HMConfig.pm martinp876 http://forum.fhem.de HomeMatic +FHEM/HttpUtils.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/MaxCommon.pm mgehre http://forum.fhem.de MAX +FHEM/ONKYOdb.pm loredo http://forum.fhem.de Multimedia +FHEM/OWX_DS2480.pm ntruchsess http://forum.fhem.de 1Wire +FHEM/OWX_DS9097.pm ntruchsess http://forum.fhem.de 1Wire +FHEM/OWX_FRM.pm ntruchsess http://forum.fhem.de 1Wire +FHEM/OWX_SER.pm ntruchsess http://forum.fhem.de 1Wire +FHEM/SetExtensions.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/SHC_datafields.pm rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/SHC_parser.pm rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/TcpServerUtils.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/lib/Device/Firmata/* ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/lib/Device/MySensors/* ntruchsess http://forum.fhem.de Sonstige Systeme +FHEM/lib/Encode/* Reinerlein http://forum.fhem.de Multimedia +FHEM/lib/MP3/* Reinerlein http://forum.fhem.de Multimedia +FHRM/lib/Normalize Reinerlein http://forum.fhem.de Multimedia +FHEM/lib/ProtoThreads.pm ntruchsess http://forum.fhem.de FHEM Development +FHEM/lib/SHC_packet_layout.xml rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/lib/SWAP/* justme1968 http://forum.fhem.de Sonstige Systeme +FHEM/lib/UPnP/* Reinerlein http://forum.fhem.de Multimedia +FHEM/FhemUtils/* mfr69bs http://forum.fhem.de Sonstiges +FHEM/GPUtils.pm ntruchsess http://forum.fhem.de FHEM Development + +contrib/YAF/* danielweisensee http://forum.fhem.de Frontends +contrib/23_WEBTHERM.pm betateilchen/sachag http://forum.fhem.de Sonstiges +contrib/55_BBB_BMP180.pm betateilchen http://forum.fhem.de Einplatinencomputer +contrib/71_LISTENLIVE.pm betateilchen http://forum.fhem.de Multimedia +contrib/98_geodata.pm betateilchen http://forum.fhem.de Sonstiges +contrib/98_openweathermap.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste +contrib/98_PID.pm betateilchen http://forum.fhem.de Automatisierung + +www/codemirror/* betateilchen http://forum.fhem.de Frontends +www/gplot/* rudolfkoenig http://forum.fhem.de Frontends +www/images/* ulimaass http://forum.fhem.de Frontends +www/pgm2/dashboard/* svenson08 http://forum.fhem.de Frontends +www/pgm2/fhemweb_readingsHistory.js justme1968 http://forum.fhem.de Frontends +www/pgm2/* rudolfkoenig http://forum.fhem.de Frontends +www/jscolor/* justme1968 http://forum.fhem.de Frontends +www/frontend/* johannnes http://forum.fhem.de Frontends + +docs/fhem-floorplan-* ulimaass http://forum.fhem.de Sonstiges +docs/* rudolfkoenig http://forum.fhem.de Sonstiges + +Files that every developer should modify/extend + MAINTAINER.txt + CHANGES + HISTORY