/// <summary> /// Ermittelt zu einer Quelle die Sonderwünsche für einen Sendersuchlauf. /// </summary> /// <param name="profile">Das betroffene Geräteprofil.</param> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die vorzunehmenden Modifikationen.</returns> /// <exception cref="ArgumentNullException">Ein Parameter wurde nicht angegeben.</exception> public static SourceModifier GetFilter(this Profile profile, SourceIdentifier source) { // Validate if (null == profile) { throw new ArgumentNullException("profile"); } if (null == source) { throw new ArgumentNullException("source"); } // Process if (null == profile.ScanConfiguration) { return new SourceModifier { Network = source.Network, TransportStream = source.TransportStream, Service = source.Service } } ; else { return(profile.ScanConfiguration.GetFilter(source)); } }
/// <summary> /// Meldet die textuelle Darstellung einer eindeutigen Kennung. /// </summary> /// <param name="source">Die gewünschte eindeutige Kennung.</param> /// <returns>Eine Textdarstellung, aufgebaut aus den einzelnen Fragmenten.</returns> public static string ToString( SourceIdentifier source ) { // Forward if (null == source) return null; else return string.Format( "({0}, {1}, {2})", source.Network, source.TransportStream, source.Service ); }
/// <summary> /// Create a new instance and start EPG parsing on PID <i>0x12</i>. /// </summary> /// <param name="profile">Related hardware device.</param> /// <param name="portal">The currently show station.</param> public ServiceParser( Profile profile, SourceIdentifier portal ) { // Remember Profile = profile; Portal = portal; // Register HardwareManager.OpenHardware( Profile ).AddProgramGuideConsumer( TableFound ); }
/// <summary> /// Create a new instance and start EPG parsing on PID <i>0x12</i>. /// </summary> /// <param name="profile">Related hardware device.</param> /// <param name="portal">The currently show station.</param> public ServiceParser(Profile profile, SourceIdentifier portal) { // Remember Profile = profile; Portal = portal; // Register HardwareManager.OpenHardware(Profile).AddProgramGuideConsumer(TableFound); }
/// <summary> /// Ermittelt die Zugriffsdaten für eine bestimmte Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die Informationen zur Quelle.</returns> private IEnumerable <SourceSelection> InternalFindSource(SourceIdentifier source) { // Get the name of the real profile var leaf = LeafProfile; // Not found if (ReferenceEquals(leaf, null)) { yield break; } // Check mode if (ReferenceEquals(leaf, this)) { // Scan us completly foreach (GroupLocation location in Locations) { foreach (SourceGroup group in location.Groups) { if (SupportsGroup(group)) { foreach (var identifier in group.Sources) { if ((source == null) || Equals(source, identifier)) { yield return new SourceSelection { DisplayName = ((Station)identifier).FullName, Location = location, Source = identifier, ProfileName = Name, Group = group } } } } } } ; } else { // Update profile name foreach (var selection in leaf.InternalFindSource(source)) { if (SupportsGroup(selection.Group)) { // Update profile association selection.ProfileName = Name; // Report yield return(selection); } } } }
/// <summary> /// Change the active station. /// </summary> /// <remarks> /// This allows the client to keep the EPG filter installed /// when changing stations. /// </remarks> /// <param name="source">The new station to focus upon.</param> public void ChangeStation(SourceIdentifier source) { // Update lock (m_SyncStation) Portal = source; // Clear lock (m_ServiceNames) m_ServiceNames.Clear(); }
/// <summary> /// Create a new instance. /// </summary> /// <param name="id">The identifier of the service.</param> /// <param name="name">The name of the service.</param> public ServiceItem( SourceIdentifier id, string name ) { // Split int split = name.IndexOf( ',' ); // Remember Index = int.Parse( name.Substring( 0, split++ ) ); Name = name.Substring( split ); Identifier = id; }
/// <summary> /// Ermittelt die Zugriffsdaten für eine bestimmte Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die Informationen zur Quelle.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public SourceSelection[] FindSource(SourceIdentifier source) { // Validate if (source == null) { throw new ArgumentNullException("source"); } else { return(InternalFindSource(source).ToArray()); } }
/// <summary> /// Meldet die textuelle Darstellung einer eindeutigen Kennung. /// </summary> /// <param name="source">Die gewünschte eindeutige Kennung.</param> /// <returns>Eine Textdarstellung, aufgebaut aus den einzelnen Fragmenten.</returns> public static string ToString(SourceIdentifier source) { // Forward if (null == source) { return(null); } else { return(string.Format("({0}, {1}, {2})", source.Network, source.TransportStream, source.Service)); } }
/// <summary> /// Ermittelt die Zugriffsdaten für eine bestimmte Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die Informationen zur Quelle.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public static SourceSelection[] FindSource(SourceIdentifier source) { // Validate if (source == null) { throw new ArgumentNullException("source"); } else { return(AllProfiles.SelectMany(profile => profile.FindSource(source)).ToArray()); } }
/// <summary> /// Versucht eine eindeutige Kennung aus der <see cref="ToString()"/> Textdarstellung /// zu rekonstruieren. /// </summary> /// <param name="text">Die vorliegende Textdarstellung.</param> /// <param name="identifier">Die eindeutige Kennung zur Textdarstellung.</param> /// <returns>Gesetzt, wenn eine eindeutige Kennung erstellt werden konnte.</returns> public static bool TryParse(string text, out SourceIdentifier identifier) { // Reset identifier = null; // None if (null == text) { return(false); } // Check it Match match = Regex.Match(text, @"^\(([ 0-9]+),([ 0-9]+),([ 0-9]+)\)$"); // No match at all if (!match.Success) { return(false); } // Try to read all parts ushort net, ts, svc; if (!ushort.TryParse(match.Groups[1].Value.Trim(), out net)) { return(false); } if (!ushort.TryParse(match.Groups[2].Value.Trim(), out ts)) { return(false); } if (!ushort.TryParse(match.Groups[3].Value.Trim(), out svc)) { return(false); } // Create identifier = new SourceIdentifier { Network = net, TransportStream = ts, Service = svc }; // Did it return(true); }
/// <summary> /// Erzeugt eine neue Verwaltung. /// </summary> /// <param name="hardware">Das Gerät, auf dem die zugehörige Quellgruppe gerade aktiv ist.</param> /// <param name="profile">Optional das Geräteprofil mit der zugehörigen Senderliste.</param> /// <param name="source">Die Quelle, die zu betrachten ist.</param> /// <param name="selection">Die zu betrachtenden Datenströme.</param> /// <exception cref="ArgumentNullException">Ein Parameter wurde nicht angegeben.</exception> public SourceStreamsManager(Hardware hardware, Profile profile, SourceIdentifier source, StreamSelection selection) { // Validate if (null == hardware) { throw new ArgumentNullException("hardware"); } if (null == source) { throw new ArgumentNullException("source"); } if (null == selection) { throw new ArgumentNullException("selection"); } // Remember all StreamSelection = selection; Hardware = hardware; Profile = profile; Source = source; }
/// <summary> /// Ermittelt die Datenstromkennung der SI Tabelle PMT zu einer Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <param name="cancel">Optional eine Abbruchsteuerung.</param> /// <returns>Die Datenstromkennung oder <i>null</i>, wenn die Quelle auf /// der aktuellen Quellgruppe (Transponder) nicht angeboten wird.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public ushort?GetServicePMT(SourceIdentifier source, CancellationToken?cancel = null) { // Validate if (source == null) { throw new ArgumentNullException("source"); } // Get the current group information var groupInfo = GetGroupInformation(cancel: cancel); if (groupInfo == null) { return(null); } // See if group exists if (!groupInfo.Sources.Any(source.Equals)) { return(null); } // Direct read of result is possible - we already have the group information available var patReader = AssociationTableReader; if (patReader == null) { return(null); } else if (!patReader.CancellableWait(cancel ?? CancellationToken.None)) { return(null); } else { return(patReader.Result.FindService(source.Service)); } }
/// <summary> /// Ermittelt zu einer Quelle die Sonderwünsche für einen Sendersuchlauf. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die vorzunehmenden Modifikationen.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public SourceModifier GetFilter(SourceIdentifier source) { // Validate if (null == source) { throw new ArgumentNullException("source"); } // Find it SourceModifier filter = (null == SourceDetails) ? null : SourceDetails.Find(f => f.Equals(source)); // Process if (null == filter) { return new SourceModifier { Network = source.Network, TransportStream = source.TransportStream, Service = source.Service } } ; else { return(filter); } }
/// <summary> /// Ermittelt die Zugriffsdaten für eine bestimmte Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die Informationen zur Quelle.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public SourceSelection[] FindSource( SourceIdentifier source ) { // Validate if (source == null) throw new ArgumentNullException( "source" ); else return InternalFindSource( source ).ToArray(); }
/// <summary> /// Parse some EPG information and try to extract the data of the current /// service group. /// </summary> /// <param name="table">Currently parsed SI table.</param> public void TableFound(EIT table) { // Test var eit = table.Table; // Not us if (null != eit) { foreach (var evt in eit.Entries) { if (evt.Status == EPG.EventStatus.Running) { // What to add var ids = new List <Station>(); var names = new List <string>(); // Make sure that this is us bool found = false; // Run over foreach (var descr in evt.Descriptors) { // Check type var info = descr as EPG.Descriptors.Linkage; if (null == info) { continue; } // Check type (PREMIERE) if (176 != info.LinkType) { continue; } // Create identifier var id = new SourceIdentifier { Network = info.OriginalNetworkIdentifier, TransportStream = info.TransportStreamIdentifier, Service = info.ServiceIdentifier }; // Lookup in profile SourceSelection[] sources = Profile.FindSource(id); if (sources.Length < 1) { continue; } // Check the first one to see if this is us if (!found) { found = Equals(id, CurrentPortal); } // Remember names.Add(string.Format("{0},{1}", names.Count, CodePage.GetString(info.PrivateData))); ids.Add((Station)sources[0].Source); } // Register if (found) { lock (m_ServiceNames) for (int i = ids.Count; i-- > 0;) { m_ServiceNames[ids[i]] = names[i]; } } } } } }
/// <summary> /// Versucht eine eindeutige Kennung aus der <see cref="ToString()"/> Textdarstellung /// zu rekonstruieren. /// </summary> /// <param name="text">Die vorliegende Textdarstellung.</param> /// <param name="identifier">Die eindeutige Kennung zur Textdarstellung.</param> /// <returns>Gesetzt, wenn eine eindeutige Kennung erstellt werden konnte.</returns> public static bool TryParse( string text, out SourceIdentifier identifier ) { // Reset identifier = null; // None if (null == text) return false; // Check it Match match = Regex.Match( text, @"^\(([ 0-9]+),([ 0-9]+),([ 0-9]+)\)$" ); // No match at all if (!match.Success) return false; // Try to read all parts ushort net, ts, svc; if (!ushort.TryParse( match.Groups[1].Value.Trim(), out net )) return false; if (!ushort.TryParse( match.Groups[2].Value.Trim(), out ts )) return false; if (!ushort.TryParse( match.Groups[3].Value.Trim(), out svc )) return false; // Create identifier = new SourceIdentifier { Network = net, TransportStream = ts, Service = svc }; // Did it return true; }
/// <summary> /// Erzeugt eine exakte Kopie einer eindeutigen Kennung. /// </summary> /// <param name="other">Die Kennung, die kopiert werden soll.</param> public SourceIdentifier( SourceIdentifier other ) : this( other.Network, other.TransportStream, other.Service ) { }
/// <summary> /// Ermittelt eine Quelle nach ihrer eindeutigen Kennung. /// </summary> /// <param name="profileName">Das zu verwendende Geräteprofil.</param> /// <param name="source">Die gewünschte Kennung.</param> /// <returns>Die eidneutige Auswahl der Quelle oder <i>null</i>.</returns> public static SourceSelection FindSource( string profileName, SourceIdentifier source ) { // No source if (source == null) return null; // No profile var state = CurrentState; if (string.IsNullOrEmpty( profileName )) if (state.Profiles.Length < 1) return null; else profileName = state.Profiles[0].Name; // Find the map Dictionary<SourceIdentifier, SourceSelection> map; if (!state.SourceByIdentifierMap.TryGetValue( profileName, out map )) return null; // Find the source SourceSelection found; if (!map.TryGetValue( source, out found )) return null; else return found; }
/// <summary> /// Erzeugt eine exakte Kopie einer eindeutigen Kennung. /// </summary> /// <param name="other">Die Kennung, die kopiert werden soll.</param> public SourceIdentifier(SourceIdentifier other) : this(other.Network, other.TransportStream, other.Service) { }
/// <summary> /// Change the active station. /// </summary> /// <remarks> /// This allows the client to keep the EPG filter installed /// when changing stations. /// </remarks> /// <param name="source">The new station to focus upon.</param> public void ChangeStation( SourceIdentifier source ) { // Update lock (m_SyncStation) Portal = source; // Clear lock (m_ServiceNames) m_ServiceNames.Clear(); }
/// <summary> /// Ermittelt die Zugriffsdaten für eine bestimmte Quelle. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die Informationen zur Quelle.</returns> private IEnumerable<SourceSelection> InternalFindSource( SourceIdentifier source ) { // Get the name of the real profile var leaf = LeafProfile; // Not found if (ReferenceEquals( leaf, null )) yield break; // Check mode if (ReferenceEquals( leaf, this )) { // Scan us completly foreach (GroupLocation location in Locations) foreach (SourceGroup group in location.Groups) if (SupportsGroup( group )) foreach (var identifier in group.Sources) if ((source == null) || Equals( source, identifier )) yield return new SourceSelection { DisplayName = ((Station) identifier).FullName, Location = location, Source = identifier, ProfileName = Name, Group = group }; } else { // Update profile name foreach (var selection in leaf.InternalFindSource( source )) if (SupportsGroup( selection.Group )) { // Update profile association selection.ProfileName = Name; // Report yield return selection; } } }
/// <summary> /// Ermittelt zu einer Quelle die Sonderwünsche für einen Sendersuchlauf. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <returns>Die vorzunehmenden Modifikationen.</returns> /// <exception cref="ArgumentNullException">Es wurde keine Quelle angegeben.</exception> public SourceModifier GetFilter( SourceIdentifier source ) { // Validate if (null == source) throw new ArgumentNullException( "source" ); // Find it SourceModifier filter = (null == SourceDetails) ? null : SourceDetails.Find( f => f.Equals( source ) ); // Process if (null == filter) return new SourceModifier { Network = source.Network, TransportStream = source.TransportStream, Service = source.Service }; else return filter; }
/// <summary> /// Beginnt mit dem Auslesen der Quelldaten. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <param name="device">Das zu verwendende Gerät.</param> /// <param name="profile">Optional das zu berücksichtigende Geräteprofil.</param> /// <returns>Die Hintergrundaufgabe zum Auslesen der Quelledaten.</returns> public static CancellableTask <SourceInformation> GetSourceInformationAsync(this Hardware device, SourceIdentifier source, Profile profile = null) { // Validate if (device == null) { throw new ArgumentNullException("no hardware to use", "device"); } if (source == null) { throw new ArgumentException("no source to get information for", "source"); } // Attach to tasks var patReader = device.AssociationTableReader; var groupReader = device.GroupReader; // Start return (CancellableTask <SourceInformation> .Run(cancel => { // Check tasks if (groupReader == null) { return null; } if (patReader == null) { return null; } // Wait on tasks if (!groupReader.CancellableWait(cancel)) { return null; } if (!patReader.CancellableWait(cancel)) { return null; } // Get the current group information var groupInfo = groupReader.Result; if (groupInfo == null) { return null; } // See if group exists if (!groupInfo.Sources.Any(source.Equals)) { return null; } // Find the stream identifier for the service var pmtIdentifier = patReader.Result.FindService(source.Service); if (!pmtIdentifier.HasValue) { return null; } // Wait for mapping table var pmtReader = device.GetTableAsync <PMT>(pmtIdentifier.Value); if (!pmtReader.CancellableWait(cancel)) { return null; } // Request table var pmts = pmtReader.Result; if (pmts == null) { return null; } // Create dummy var currentSettings = new SourceInformation { Source = source, VideoType = VideoTypes.NoVideo }; // Process all PMT - actually should be only one foreach (var pmt in pmts) { // Overwrite encryption if CA descriptor is present if (pmt.Table.Descriptors != null) { currentSettings.IsEncrypted = pmt.Table.Descriptors.Any(descriptor => EPG.DescriptorTags.CA == descriptor.Tag); } // Process the program entries foreach (var program in pmt.Table.ProgramEntries) { currentSettings.Update(program); } } // Find the related station information var station = groupInfo.Sources.FirstOrDefault(source.Equals) as Station; if (station != null) { // Take data from there currentSettings.Provider = station.Provider; currentSettings.Name = station.Name; // See if this is a service currentSettings.IsService = station.IsService; // Overwrite encryption if regular service entry exists if (!currentSettings.IsService) { currentSettings.IsEncrypted = station.IsEncrypted; } } // See if profile is attached if (profile != null) { // Apply the modifier var modifier = profile.GetFilter(currentSettings.Source); if (modifier != null) { modifier.ApplyTo(currentSettings); } } // Report return currentSettings; })); }
/// <summary> /// Aktiviert den Netzwerkversand für eine aktive Quelle. /// </summary> /// <param name="source">Die betroffene Quelle.</param> /// <param name="uniqueIdentifier">Die eindeutige Kennung der Teilaufzeichnung.</param> /// <param name="streamingTarget">Das neue Ziel für den Netzwerkversand.</param> public void SetStreamTarget( SourceIdentifier source, Guid uniqueIdentifier, string streamingTarget ) { // Validate if (source == null) throw new ArgumentNullException( "source" ); // Report Tools.ExtendedLogging( "Changing Streaming for {0} [{1}] to {2}", source, uniqueIdentifier, streamingTarget ); // Process EnqueueActionAndWait( () => ServerImplementation.EndRequest( Server.BeginSetStreamTarget( source, uniqueIdentifier, streamingTarget ) ) ); }
/// <summary> /// Beginnt mit dem Auslesen der Quelldaten. /// </summary> /// <param name="source">Die gewünschte Quelle.</param> /// <param name="device">Das zu verwendende Gerät.</param> /// <param name="profile">Optional das zu berücksichtigende Geräteprofil.</param> /// <returns>Die Hintergrundaufgabe zum Auslesen der Quelledaten.</returns> public static CancellableTask<SourceInformation> GetSourceInformationAsync( this Hardware device, SourceIdentifier source, Profile profile = null ) { // Validate if (device == null) throw new ArgumentNullException( "no hardware to use", "device" ); if (source == null) throw new ArgumentException( "no source to get information for", "source" ); // Attach to tasks var patReader = device.AssociationTableReader; var groupReader = device.GroupReader; // Start return CancellableTask<SourceInformation>.Run( cancel => { // Check tasks if (groupReader == null) return null; if (patReader == null) return null; // Wait on tasks if (!groupReader.CancellableWait( cancel )) return null; if (!patReader.CancellableWait( cancel )) return null; // Get the current group information var groupInfo = groupReader.Result; if (groupInfo == null) return null; // See if group exists if (!groupInfo.Sources.Any( source.Equals )) return null; // Find the stream identifier for the service var pmtIdentifier = patReader.Result.FindService( source.Service ); if (!pmtIdentifier.HasValue) return null; // Wait for mapping table var pmtReader = device.GetTableAsync<PMT>( pmtIdentifier.Value ); if (!pmtReader.CancellableWait( cancel )) return null; // Request table var pmts = pmtReader.Result; if (pmts == null) return null; // Create dummy var currentSettings = new SourceInformation { Source = source, VideoType = VideoTypes.NoVideo }; // Process all PMT - actually should be only one foreach (var pmt in pmts) { // Overwrite encryption if CA descriptor is present if (pmt.Table.Descriptors != null) currentSettings.IsEncrypted = pmt.Table.Descriptors.Any( descriptor => EPG.DescriptorTags.CA == descriptor.Tag ); // Process the program entries foreach (var program in pmt.Table.ProgramEntries) currentSettings.Update( program ); } // Find the related station information var station = groupInfo.Sources.FirstOrDefault( source.Equals ) as Station; if (station != null) { // Take data from there currentSettings.Provider = station.Provider; currentSettings.Name = station.Name; // See if this is a service currentSettings.IsService = station.IsService; // Overwrite encryption if regular service entry exists if (!currentSettings.IsService) currentSettings.IsEncrypted = station.IsEncrypted; } // See if profile is attached if (profile != null) { // Apply the modifier var modifier = profile.GetFilter( currentSettings.Source ); if (modifier != null) modifier.ApplyTo( currentSettings ); } // Report return currentSettings; } ); }
/// <summary> /// Parse some EPG information and try to extract the data of the current /// service group. /// </summary> /// <param name="table">Currently parsed SI table.</param> public void TableFound( EIT table ) { // Test var eit = table.Table; // Not us if (null != eit) foreach (var evt in eit.Entries) if (evt.Status == EPG.EventStatus.Running) { // What to add var ids = new List<Station>(); var names = new List<string>(); // Make sure that this is us bool found = false; // Run over foreach (var descr in evt.Descriptors) { // Check type var info = descr as EPG.Descriptors.Linkage; if (null == info) continue; // Check type (PREMIERE) if (176 != info.LinkType) continue; // Create identifier var id = new SourceIdentifier { Network = info.OriginalNetworkIdentifier, TransportStream = info.TransportStreamIdentifier, Service = info.ServiceIdentifier }; // Lookup in profile SourceSelection[] sources = Profile.FindSource( id ); if (sources.Length < 1) continue; // Check the first one to see if this is us if (!found) found = Equals( id, CurrentPortal ); // Remember names.Add( string.Format( "{0},{1}", names.Count, CodePage.GetString( info.PrivateData ) ) ); ids.Add( (Station) sources[0].Source ); } // Register if (found) lock (m_ServiceNames) for (int i = ids.Count; i-- > 0; ) m_ServiceNames[ids[i]] = names[i]; } }
/// <summary> /// Erzeugt eine neue Verwaltung. /// </summary> /// <param name="hardware">Das Gerät, auf dem die zugehörige Quellgruppe gerade aktiv ist.</param> /// <param name="profile">Optional das Geräteprofil mit der zugehörigen Senderliste.</param> /// <param name="source">Die Quelle, die zu betrachten ist.</param> /// <param name="selection">Die zu betrachtenden Datenströme.</param> /// <exception cref="ArgumentNullException">Ein Parameter wurde nicht angegeben.</exception> public SourceStreamsManager( Hardware hardware, Profile profile, SourceIdentifier source, StreamSelection selection ) { // Validate if (null == hardware) throw new ArgumentNullException( "hardware" ); if (null == source) throw new ArgumentNullException( "source" ); if (null == selection) throw new ArgumentNullException( "selection" ); // Remember all StreamSelection = selection; Hardware = hardware; Profile = profile; Source = source; }
/// <summary> /// Erzeugt einen Aufzeichnungskontext für eine Quelle. /// </summary> /// <param name="source">Die Informationen zur Quelle.</param> /// <param name="hardware">Das zu verwendende Gerät.</param> /// <param name="profile">Opetional ein Geräteprofil mit der zugehörigen Senderliste.</param> /// <param name="streams">Die gewünschten Aufzeichnungsparameter.</param> /// <returns>Eine Kontrollinstanz für die Aufzeichnung. Diese muss mittels <see cref="IDisposable.Dispose"/> /// freigegeben werden.</returns> public static SourceStreamsManager Open(this SourceIdentifier source, Hardware hardware, Profile profile, StreamSelection streams) { // Forward return(new SourceStreamsManager(hardware, profile, source, streams)); }